bahi-bh commited on
Commit
cd991bf
·
verified ·
1 Parent(s): 071cc9f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +261 -171
app.py CHANGED
@@ -1,3 +1,8 @@
 
 
 
 
 
1
  import os
2
  import json
3
  import uuid
@@ -76,6 +81,7 @@ app = FastAPI(lifespan=lifespan)
76
  @app.get("/")
77
  @app.head("/")
78
  async def root():
 
79
  return {
80
  "status": "ok",
81
  "service": "OpenCode Gateway"
@@ -83,6 +89,7 @@ async def root():
83
 
84
  @app.get("/health")
85
  async def health():
 
86
  return {
87
  "status": "healthy"
88
  }
@@ -165,9 +172,17 @@ def normalize_messages(raw_messages):
165
 
166
  for msg in raw_messages:
167
 
168
- role = msg.get("role", "user")
169
- content = msg.get("content", "")
 
 
170
 
 
 
 
 
 
 
171
  if isinstance(content, list):
172
 
173
  text_parts = []
@@ -177,11 +192,13 @@ def normalize_messages(raw_messages):
177
  if isinstance(item, dict):
178
 
179
  if item.get("type") == "text":
 
180
  text_parts.append(
181
  item.get("text", "")
182
  )
183
 
184
  elif isinstance(item, str):
 
185
  text_parts.append(item)
186
 
187
  content = "\n".join(text_parts)
@@ -194,7 +211,38 @@ def normalize_messages(raw_messages):
194
  return normalized
195
 
196
  # =========================================================
197
- # OPENAI -> ANTHROPIC RESPONSE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  # =========================================================
199
  def convert_to_anthropic_format(
200
  parsed,
@@ -205,7 +253,10 @@ def convert_to_anthropic_format(
205
 
206
  try:
207
 
208
- choices = parsed.get("choices", [])
 
 
 
209
 
210
  if choices:
211
 
@@ -223,18 +274,26 @@ def convert_to_anthropic_format(
223
  pass
224
 
225
  return {
 
226
  "id": f"msg_{uuid.uuid4().hex}",
 
227
  "type": "message",
 
228
  "role": "assistant",
 
229
  "model": requested_model,
 
230
  "content": [
231
  {
232
  "type": "text",
233
  "text": content_text
234
  }
235
  ],
 
236
  "stop_reason": "end_turn",
 
237
  "stop_sequence": None,
 
238
  "usage": {
239
  "input_tokens": 0,
240
  "output_tokens": 0
@@ -242,56 +301,71 @@ def convert_to_anthropic_format(
242
  }
243
 
244
  # =========================================================
245
- # STREAM TRANSLATOR
246
  # =========================================================
247
- async def stream_sse_response(
248
  response,
249
  requested_model
250
  ):
251
 
252
- try:
 
 
 
 
 
 
253
 
254
- # بداية الرسالة
255
- start_payload = {
256
  "type": "message_start",
 
257
  "message": {
258
- "id": f"msg_{uuid.uuid4().hex}",
 
 
259
  "type": "message",
 
260
  "role": "assistant",
 
261
  "model": requested_model,
 
262
  "content": [],
 
263
  "stop_reason": None,
 
264
  "stop_sequence": None,
 
265
  "usage": {
266
  "input_tokens": 0,
267
  "output_tokens": 0
268
  }
269
  }
270
- }
271
 
272
- yield (
273
- "event: message_start\n"
274
- "data: "
275
- + json.dumps(start_payload)
276
- + "\n\n"
277
- )
 
 
 
278
 
279
- # بداية البلوك
280
- block_payload = {
281
  "type": "content_block_start",
 
282
  "index": 0,
 
283
  "content_block": {
 
284
  "type": "text",
 
285
  "text": ""
286
  }
287
- }
288
 
289
- yield (
290
- "event: content_block_start\n"
291
- "data: "
292
- + json.dumps(block_payload)
293
- + "\n\n"
294
- )
295
 
296
  async for line in response.aiter_lines():
297
 
@@ -315,7 +389,10 @@ async def stream_sse_response(
315
  except Exception:
316
  continue
317
 
318
- choices = parsed.get("choices", [])
 
 
 
319
 
320
  if not choices:
321
  continue
@@ -333,81 +410,95 @@ async def stream_sse_response(
333
  if not text:
334
  continue
335
 
336
- delta_payload = {
337
- "type": "content_block_delta",
338
- "index": 0,
339
- "delta": {
340
- "type": "text_delta",
341
- "text": text
342
- }
343
- }
344
-
345
  yield (
346
  "event: content_block_delta\n"
347
  "data: "
348
- + json.dumps(delta_payload)
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  + "\n\n"
350
  )
351
 
352
- # نهاية البلوك
353
- end_block_payload = {
354
- "type": "content_block_stop",
355
- "index": 0
356
- }
357
 
358
  yield (
359
- "event: content_block_stop\n"
360
  "data: "
361
- + json.dumps(end_block_payload)
 
 
 
 
 
 
 
 
 
 
 
362
  + "\n\n"
363
  )
364
 
365
- # دلتا الرسالة
366
- delta_payload = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  "type": "message_delta",
 
368
  "delta": {
 
369
  "stop_reason": "end_turn",
 
370
  "stop_sequence": None
371
  },
 
372
  "usage": {
 
373
  "output_tokens": 0
374
  }
375
- }
376
-
377
- yield (
378
- "event: message_delta\n"
379
- "data: "
380
- + json.dumps(delta_payload)
381
- + "\n\n"
382
- )
383
-
384
- # نهاية الرسالة
385
- yield (
386
- "event: message_stop\n"
387
- "data: {}\n\n"
388
- )
389
 
390
- except Exception as e:
391
-
392
- logger.exception("STREAM ERROR")
393
-
394
- error_payload = {
395
- "type": "error",
396
- "error": {
397
- "type": "stream_error",
398
- "message": str(e)
399
- }
400
- }
401
 
402
- yield (
403
- "event: error\n"
404
- "data: "
405
- + json.dumps(error_payload)
406
- + "\n\n"
407
- )
408
 
409
  # =========================================================
410
- # MAIN ROUTES
411
  # =========================================================
412
  @app.post("/v1/messages")
413
  @app.post("/v1/chat/completions")
@@ -427,18 +518,17 @@ async def messages(
427
  # Bearer
428
  if authorization:
429
 
430
- auth_value = authorization.strip()
431
-
432
- if auth_value.lower().startswith("bearer "):
433
 
434
- api_key = auth_value[7:].strip()
435
-
436
- else:
437
-
438
- api_key = auth_value.strip()
439
 
440
  # x-api-key
441
  if x_api_key:
 
442
  api_key = x_api_key.strip()
443
 
444
  if not api_key:
@@ -446,15 +536,12 @@ async def messages(
446
  return JSONResponse(
447
  status_code=401,
448
  content={
449
- "error": {
450
- "type": "auth_error",
451
- "message": "Missing API key"
452
- }
453
  }
454
  )
455
 
456
  # =====================================================
457
- # JSON BODY
458
  # =====================================================
459
  try:
460
 
@@ -500,13 +587,18 @@ async def messages(
500
  # PAYLOAD
501
  # =====================================================
502
  payload = {
 
503
  "model": mapped_model,
 
504
  "messages": normalized_messages,
 
505
  "stream": stream,
 
506
  "temperature": body.get(
507
  "temperature",
508
  0.7
509
  ),
 
510
  "max_tokens": min(
511
  body.get("max_tokens", 4096),
512
  4096
@@ -514,27 +606,39 @@ async def messages(
514
  }
515
 
516
  # =====================================================
517
- # PASS THROUGH FIELDS
518
  # =====================================================
519
  passthrough_fields = [
520
 
521
  "tools",
522
  "tool_choice",
523
- "system",
524
- "metadata",
525
- "betas",
526
  "mcp_servers",
527
  "plugins",
528
- "web_search",
529
- "response_format",
530
- "parallel_tool_calls"
531
  ]
532
 
533
  for field in passthrough_fields:
534
 
535
  if field in body:
 
536
  payload[field] = body[field]
537
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  # =====================================================
539
  # LOGS
540
  # =====================================================
@@ -555,15 +659,18 @@ async def messages(
555
  # =====================================================
556
  headers = {
557
 
 
558
  "x-api-key": api_key,
559
 
560
  "Authorization": f"Bearer {api_key}",
561
 
562
- "anthropic-version": "2023-06-01",
563
-
564
  "Content-Type": "application/json",
565
 
566
- "Accept": "text/event-stream" if stream else "application/json",
 
 
 
 
567
 
568
  "Origin": "https://opencode.ai",
569
 
@@ -591,44 +698,53 @@ async def messages(
591
  f"[{req_id}] Attempt {attempt}"
592
  )
593
 
594
- # =================================================
595
- # STREAM MODE
596
- # =================================================
597
- if stream:
 
598
 
599
- response = await client.post(
600
- UPSTREAM_URL,
601
- json=payload,
602
- headers=headers
603
- )
604
 
605
- logger.info(
606
- f"[{req_id}] STATUS -> {response.status_code}"
607
- )
608
 
609
- raw = response.text[:1000]
 
 
 
610
 
611
- if response.status_code != 200:
 
 
612
 
613
- logger.warning(
614
- f"[{req_id}] FAILED -> {raw}"
615
- )
616
 
617
- last_error = raw
618
 
619
- await asyncio.sleep(RETRY_DELAY)
620
 
621
- continue
 
 
 
622
 
623
  return StreamingResponse(
624
- stream_sse_response(
 
625
  response,
626
  requested_model
627
  ),
 
628
  media_type="text/event-stream",
 
629
  headers={
 
630
  "Cache-Control": "no-cache",
 
631
  "Connection": "keep-alive",
 
632
  "X-Accel-Buffering": "no"
633
  }
634
  )
@@ -636,64 +752,38 @@ async def messages(
636
  # =================================================
637
  # NORMAL MODE
638
  # =================================================
639
- else:
640
-
641
- response = await client.post(
642
- UPSTREAM_URL,
643
- json=payload,
644
- headers=headers
645
- )
646
 
647
- logger.info(
648
- f"[{req_id}] STATUS -> {response.status_code}"
 
 
 
649
  )
650
 
651
- raw = response.text.strip()
652
-
653
- if response.status_code != 200:
654
-
655
- logger.warning(
656
- f"[{req_id}] FAILED -> {raw}"
657
- )
658
-
659
- last_error = raw
660
-
661
- await asyncio.sleep(RETRY_DELAY)
662
-
663
- continue
664
-
665
- if not raw:
666
-
667
- return JSONResponse(
668
- status_code=502,
669
- content={
670
- "error": "Empty upstream response"
671
- }
672
- )
673
-
674
- try:
675
-
676
- parsed = response.json()
677
-
678
- except Exception:
679
 
680
- return JSONResponse(
681
- status_code=502,
682
- content={
683
- "error": "Invalid JSON",
684
- "raw": raw
685
- }
686
- )
687
 
688
- anthropic_response = convert_to_anthropic_format(
689
- parsed,
690
- requested_model
691
- )
692
 
693
  return JSONResponse(
694
- content=anthropic_response
 
 
 
 
695
  )
696
 
 
 
 
 
 
 
 
 
 
697
  except Exception as e:
698
 
699
  last_error = str(e)
 
1
+ # =========================================================
2
+ # OPENCODE GATEWAY
3
+ # FIXED INTERNET + MCP + ANTHROPIC FORMAT
4
+ # =========================================================
5
+
6
  import os
7
  import json
8
  import uuid
 
81
  @app.get("/")
82
  @app.head("/")
83
  async def root():
84
+
85
  return {
86
  "status": "ok",
87
  "service": "OpenCode Gateway"
 
89
 
90
  @app.get("/health")
91
  async def health():
92
+
93
  return {
94
  "status": "healthy"
95
  }
 
172
 
173
  for msg in raw_messages:
174
 
175
+ role = msg.get(
176
+ "role",
177
+ "user"
178
+ )
179
 
180
+ content = msg.get(
181
+ "content",
182
+ ""
183
+ )
184
+
185
+ # Anthropic blocks
186
  if isinstance(content, list):
187
 
188
  text_parts = []
 
192
  if isinstance(item, dict):
193
 
194
  if item.get("type") == "text":
195
+
196
  text_parts.append(
197
  item.get("text", "")
198
  )
199
 
200
  elif isinstance(item, str):
201
+
202
  text_parts.append(item)
203
 
204
  content = "\n".join(text_parts)
 
211
  return normalized
212
 
213
  # =========================================================
214
+ # REMOVE CLAUDE INTERNAL TOOLS
215
+ # =========================================================
216
+ def clean_tools(tools):
217
+
218
+ allowed_tools = []
219
+
220
+ blocked = {
221
+
222
+ "Agent",
223
+ "AskUserQuestion",
224
+ "Edit",
225
+ "Glob",
226
+ "Grep",
227
+ "NotebookEdit",
228
+ "ExitPlanMode",
229
+ "Read",
230
+ "Write"
231
+ }
232
+
233
+ for tool in tools:
234
+
235
+ name = tool.get("name", "")
236
+
237
+ if name in blocked:
238
+ continue
239
+
240
+ allowed_tools.append(tool)
241
+
242
+ return allowed_tools
243
+
244
+ # =========================================================
245
+ # OPENAI -> ANTHROPIC
246
  # =========================================================
247
  def convert_to_anthropic_format(
248
  parsed,
 
253
 
254
  try:
255
 
256
+ choices = parsed.get(
257
+ "choices",
258
+ []
259
+ )
260
 
261
  if choices:
262
 
 
274
  pass
275
 
276
  return {
277
+
278
  "id": f"msg_{uuid.uuid4().hex}",
279
+
280
  "type": "message",
281
+
282
  "role": "assistant",
283
+
284
  "model": requested_model,
285
+
286
  "content": [
287
  {
288
  "type": "text",
289
  "text": content_text
290
  }
291
  ],
292
+
293
  "stop_reason": "end_turn",
294
+
295
  "stop_sequence": None,
296
+
297
  "usage": {
298
  "input_tokens": 0,
299
  "output_tokens": 0
 
301
  }
302
 
303
  # =========================================================
304
+ # STREAM CONVERTER
305
  # =========================================================
306
+ async def anthropic_stream_response(
307
  response,
308
  requested_model
309
  ):
310
 
311
+ message_id = f"msg_{uuid.uuid4().hex}"
312
+
313
+ # MESSAGE START
314
+ yield (
315
+ "event: message_start\n"
316
+ "data: "
317
+ + json.dumps({
318
 
 
 
319
  "type": "message_start",
320
+
321
  "message": {
322
+
323
+ "id": message_id,
324
+
325
  "type": "message",
326
+
327
  "role": "assistant",
328
+
329
  "model": requested_model,
330
+
331
  "content": [],
332
+
333
  "stop_reason": None,
334
+
335
  "stop_sequence": None,
336
+
337
  "usage": {
338
  "input_tokens": 0,
339
  "output_tokens": 0
340
  }
341
  }
 
342
 
343
+ })
344
+ + "\n\n"
345
+ )
346
+
347
+ # CONTENT BLOCK START
348
+ yield (
349
+ "event: content_block_start\n"
350
+ "data: "
351
+ + json.dumps({
352
 
 
 
353
  "type": "content_block_start",
354
+
355
  "index": 0,
356
+
357
  "content_block": {
358
+
359
  "type": "text",
360
+
361
  "text": ""
362
  }
 
363
 
364
+ })
365
+ + "\n\n"
366
+ )
367
+
368
+ try:
 
369
 
370
  async for line in response.aiter_lines():
371
 
 
389
  except Exception:
390
  continue
391
 
392
+ choices = parsed.get(
393
+ "choices",
394
+ []
395
+ )
396
 
397
  if not choices:
398
  continue
 
410
  if not text:
411
  continue
412
 
413
+ # CONTENT DELTA
 
 
 
 
 
 
 
 
414
  yield (
415
  "event: content_block_delta\n"
416
  "data: "
417
+ + json.dumps({
418
+
419
+ "type": "content_block_delta",
420
+
421
+ "index": 0,
422
+
423
+ "delta": {
424
+
425
+ "type": "text_delta",
426
+
427
+ "text": text
428
+ }
429
+
430
+ })
431
  + "\n\n"
432
  )
433
 
434
+ except Exception as e:
435
+
436
+ logger.exception("STREAM ERROR")
 
 
437
 
438
  yield (
439
+ "event: error\n"
440
  "data: "
441
+ + json.dumps({
442
+
443
+ "type": "error",
444
+
445
+ "error": {
446
+
447
+ "type": "stream_error",
448
+
449
+ "message": str(e)
450
+ }
451
+
452
+ })
453
  + "\n\n"
454
  )
455
 
456
+ # CONTENT STOP
457
+ yield (
458
+ "event: content_block_stop\n"
459
+ "data: "
460
+ + json.dumps({
461
+
462
+ "type": "content_block_stop",
463
+
464
+ "index": 0
465
+
466
+ })
467
+ + "\n\n"
468
+ )
469
+
470
+ # MESSAGE DELTA
471
+ yield (
472
+ "event: message_delta\n"
473
+ "data: "
474
+ + json.dumps({
475
+
476
  "type": "message_delta",
477
+
478
  "delta": {
479
+
480
  "stop_reason": "end_turn",
481
+
482
  "stop_sequence": None
483
  },
484
+
485
  "usage": {
486
+
487
  "output_tokens": 0
488
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
 
490
+ })
491
+ + "\n\n"
492
+ )
 
 
 
 
 
 
 
 
493
 
494
+ # MESSAGE STOP
495
+ yield (
496
+ "event: message_stop\n"
497
+ "data: {}\n\n"
498
+ )
 
499
 
500
  # =========================================================
501
+ # MAIN ROUTE
502
  # =========================================================
503
  @app.post("/v1/messages")
504
  @app.post("/v1/chat/completions")
 
518
  # Bearer
519
  if authorization:
520
 
521
+ if authorization.startswith("Bearer "):
 
 
522
 
523
+ api_key = (
524
+ authorization
525
+ .replace("Bearer ", "")
526
+ .strip()
527
+ )
528
 
529
  # x-api-key
530
  if x_api_key:
531
+
532
  api_key = x_api_key.strip()
533
 
534
  if not api_key:
 
536
  return JSONResponse(
537
  status_code=401,
538
  content={
539
+ "error": "Missing API key"
 
 
 
540
  }
541
  )
542
 
543
  # =====================================================
544
+ # BODY
545
  # =====================================================
546
  try:
547
 
 
587
  # PAYLOAD
588
  # =====================================================
589
  payload = {
590
+
591
  "model": mapped_model,
592
+
593
  "messages": normalized_messages,
594
+
595
  "stream": stream,
596
+
597
  "temperature": body.get(
598
  "temperature",
599
  0.7
600
  ),
601
+
602
  "max_tokens": min(
603
  body.get("max_tokens", 4096),
604
  4096
 
606
  }
607
 
608
  # =====================================================
609
+ # KEEP MCP + INTERNET
610
  # =====================================================
611
  passthrough_fields = [
612
 
613
  "tools",
614
  "tool_choice",
615
+ "web_search",
 
 
616
  "mcp_servers",
617
  "plugins",
618
+ "parallel_tool_calls",
619
+ "response_format"
 
620
  ]
621
 
622
  for field in passthrough_fields:
623
 
624
  if field in body:
625
+
626
  payload[field] = body[field]
627
 
628
+ # =====================================================
629
+ # CLEAN BROKEN CLAUDE TOOLS ONLY
630
+ # =====================================================
631
+ if "tools" in payload:
632
+
633
+ payload["tools"] = clean_tools(
634
+ payload["tools"]
635
+ )
636
+
637
+ # =====================================================
638
+ # ENABLE INTERNET
639
+ # =====================================================
640
+ payload["web_search"] = True
641
+
642
  # =====================================================
643
  # LOGS
644
  # =====================================================
 
659
  # =====================================================
660
  headers = {
661
 
662
+ # IMPORTANT
663
  "x-api-key": api_key,
664
 
665
  "Authorization": f"Bearer {api_key}",
666
 
 
 
667
  "Content-Type": "application/json",
668
 
669
+ "Accept": (
670
+ "text/event-stream"
671
+ if stream
672
+ else "application/json"
673
+ ),
674
 
675
  "Origin": "https://opencode.ai",
676
 
 
698
  f"[{req_id}] Attempt {attempt}"
699
  )
700
 
701
+ response = await client.post(
702
+ UPSTREAM_URL,
703
+ json=payload,
704
+ headers=headers
705
+ )
706
 
707
+ logger.info(
708
+ f"[{req_id}] STATUS -> {response.status_code}"
709
+ )
 
 
710
 
711
+ raw = response.text.strip()
 
 
712
 
713
+ # =================================================
714
+ # FAIL
715
+ # =================================================
716
+ if response.status_code != 200:
717
 
718
+ logger.warning(
719
+ f"[{req_id}] FAILED -> {raw}"
720
+ )
721
 
722
+ last_error = raw
 
 
723
 
724
+ await asyncio.sleep(RETRY_DELAY)
725
 
726
+ continue
727
 
728
+ # =================================================
729
+ # STREAM MODE
730
+ # =================================================
731
+ if stream:
732
 
733
  return StreamingResponse(
734
+
735
+ anthropic_stream_response(
736
  response,
737
  requested_model
738
  ),
739
+
740
  media_type="text/event-stream",
741
+
742
  headers={
743
+
744
  "Cache-Control": "no-cache",
745
+
746
  "Connection": "keep-alive",
747
+
748
  "X-Accel-Buffering": "no"
749
  }
750
  )
 
752
  # =================================================
753
  # NORMAL MODE
754
  # =================================================
755
+ if not raw:
 
 
 
 
 
 
756
 
757
+ return JSONResponse(
758
+ status_code=502,
759
+ content={
760
+ "error": "Empty upstream response"
761
+ }
762
  )
763
 
764
+ try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
765
 
766
+ parsed = response.json()
 
 
 
 
 
 
767
 
768
+ except Exception:
 
 
 
769
 
770
  return JSONResponse(
771
+ status_code=502,
772
+ content={
773
+ "error": "Invalid JSON",
774
+ "raw": raw
775
+ }
776
  )
777
 
778
+ anthropic_response = convert_to_anthropic_format(
779
+ parsed,
780
+ requested_model
781
+ )
782
+
783
+ return JSONResponse(
784
+ content=anthropic_response
785
+ )
786
+
787
  except Exception as e:
788
 
789
  last_error = str(e)