Files changed (1) hide show
  1. app.py +329 -5
app.py CHANGED
@@ -636,20 +636,344 @@ def test_onyx_connection():
636
  return jsonify(results)
637
 
638
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
  @app.route('/', methods=['GET'])
640
  def root():
641
  """Root endpoint with API info"""
642
  return jsonify({
643
- "name": "OpenAI-Compatible",
644
- "version": "1.0.0",
645
  "endpoints": {
646
- "chat_completions": "/v1/chat/completions",
 
647
  "models": "/v1/models",
648
  "sessions": "/v1/sessions",
649
  "health": "/health",
650
- "debug": ""
651
  },
652
- "model_format": "provider/model_version (e.g., openai/gpt-4)"
653
  })
654
 
655
 
 
636
  return jsonify(results)
637
 
638
 
639
+ # ============== Anthropic Messages API ==============
640
+
641
+ def build_anthropic_payload_from_messages(messages, system_prompt, model_provider, model_version, temperature, chat_session_id, parent_message_id=None, stream=True, tools=None):
642
+ """Convert Anthropic Messages API format to Onyx payload"""
643
+
644
+ # Extract the last user message
645
+ last_user_message = ""
646
+ for msg in reversed(messages):
647
+ if msg.get('role') == 'user':
648
+ content = msg.get('content', '')
649
+ if isinstance(content, list):
650
+ text_parts = [p.get('text', '') for p in content if p.get('type') == 'text']
651
+ last_user_message = ' '.join(text_parts)
652
+ elif isinstance(content, str):
653
+ last_user_message = content
654
+ break
655
+
656
+ # Build full message with system prompt
657
+ full_message = last_user_message
658
+ if system_prompt:
659
+ if isinstance(system_prompt, list):
660
+ sys_text = ' '.join([s.get('text', '') for s in system_prompt if s.get('type') == 'text'])
661
+ else:
662
+ sys_text = system_prompt
663
+ full_message = f"[System: {sys_text}]\n\n{last_user_message}"
664
+
665
+ # If tools are provided, inject them into the prompt context
666
+ if tools:
667
+ tools_desc = "\n\n[Available Tools:\n"
668
+ for tool in tools:
669
+ name = tool.get('name', '')
670
+ desc = tool.get('description', '')
671
+ input_schema = json.dumps(tool.get('input_schema', {}), indent=2)
672
+ tools_desc += f"- {name}: {desc}\n Input Schema: {input_schema}\n"
673
+ tools_desc += "]\n\n"
674
+ full_message = tools_desc + full_message
675
+
676
+ payload = {
677
+ "message": full_message,
678
+ "chat_session_id": chat_session_id,
679
+ "parent_message_id": parent_message_id if parent_message_id else None,
680
+ "stream": stream,
681
+ "llm_override": {
682
+ "model_provider": model_provider,
683
+ "model_version": model_version,
684
+ "temperature": temperature
685
+ },
686
+ "file_descriptors": [],
687
+ "include_citations": False
688
+ }
689
+
690
+ return payload
691
+
692
+
693
+ def generate_anthropic_stream_events(payload, model, session_key):
694
+ """Stream response from Onyx in Anthropic Messages SSE format"""
695
+
696
+ msg_id = f"msg_{uuid.uuid4().hex[:24]}"
697
+ final_message_id = None
698
+
699
+ endpoints = [
700
+ f"{ONYX_BASE_URL}/api/chat/send-chat-message",
701
+ f"{ONYX_BASE_URL}/api/chat/send-message",
702
+ ]
703
+
704
+ # message_start event
705
+ msg_start = {
706
+ "type": "message_start",
707
+ "message": {
708
+ "id": msg_id,
709
+ "type": "message",
710
+ "role": "assistant",
711
+ "content": [],
712
+ "model": model,
713
+ "stop_reason": None,
714
+ "stop_sequence": None,
715
+ "usage": {"input_tokens": 0, "output_tokens": 0}
716
+ }
717
+ }
718
+ yield f"event: message_start\ndata: {json.dumps(msg_start)}\n\n"
719
+
720
+ # content_block_start
721
+ yield f"event: content_block_start\ndata: {json.dumps({'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'text', 'text': ''}})}\n\n"
722
+
723
+ # Ping
724
+ yield f"event: ping\ndata: {json.dumps({'type': 'ping'})}\n\n"
725
+
726
+ last_msg_id = None
727
+
728
+ for url in endpoints:
729
+ try:
730
+ with requests.post(url, json=payload, headers=get_headers(), stream=True, timeout=120) as response:
731
+ if response.status_code != 200:
732
+ continue
733
+
734
+ buffer = ""
735
+ for chunk in response.iter_content(decode_unicode=True):
736
+ if not chunk:
737
+ continue
738
+ buffer += chunk
739
+
740
+ while '\n' in buffer:
741
+ line, buffer = buffer.split('\n', 1)
742
+ line = line.strip()
743
+
744
+ if not line or line == "[DONE]":
745
+ continue
746
+ if line.startswith("data: "):
747
+ line = line[6:]
748
+
749
+ content, m_id, packet_type = parse_onyx_stream_chunk(line)
750
+
751
+ if m_id:
752
+ last_msg_id = m_id
753
+
754
+ if content and packet_type in ['content', 'legacy', 'raw']:
755
+ delta_event = {
756
+ "type": "content_block_delta",
757
+ "index": 0,
758
+ "delta": {"type": "text_delta", "text": content}
759
+ }
760
+ yield f"event: content_block_delta\ndata: {json.dumps(delta_event)}\n\n"
761
+
762
+ if packet_type == "stop":
763
+ final_message_id = last_msg_id
764
+ break
765
+
766
+ break
767
+ except Exception as e:
768
+ print(f"Anthropic stream error: {e}")
769
+ continue
770
+
771
+ # Update session
772
+ if final_message_id and session_key in chat_sessions_cache:
773
+ chat_sessions_cache[session_key]["parent_message_id"] = final_message_id
774
+
775
+ # content_block_stop
776
+ yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
777
+
778
+ # message_delta (stop reason)
779
+ msg_delta = {
780
+ "type": "message_delta",
781
+ "delta": {"stop_reason": "end_turn", "stop_sequence": None},
782
+ "usage": {"output_tokens": 0}
783
+ }
784
+ yield f"event: message_delta\ndata: {json.dumps(msg_delta)}\n\n"
785
+
786
+ # message_stop
787
+ yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n"
788
+
789
+
790
+ def collect_anthropic_full_response(payload, model, session_key):
791
+ """Collect full response and return in Anthropic Messages format"""
792
+
793
+ full_content = ""
794
+ last_message_id = None
795
+
796
+ endpoints = [
797
+ f"{ONYX_BASE_URL}/api/chat/send-chat-message",
798
+ f"{ONYX_BASE_URL}/api/chat/send-message",
799
+ ]
800
+
801
+ for url in endpoints:
802
+ try:
803
+ is_streaming_request = payload.get('stream', False)
804
+
805
+ with requests.post(url, json=payload, headers=get_headers(), stream=is_streaming_request, timeout=120) as response:
806
+ if response.status_code == 404:
807
+ continue
808
+
809
+ if response.status_code != 200:
810
+ return {
811
+ "type": "error",
812
+ "error": {
813
+ "type": "api_error",
814
+ "message": f"Onyx API error {response.status_code}: {response.text}"
815
+ }
816
+ }, response.status_code
817
+
818
+ if not is_streaming_request:
819
+ try:
820
+ data = response.json()
821
+ full_content = data.get('answer') or data.get('message') or data.get('content') or ""
822
+ msg_id = data.get('message_id')
823
+ if session_key in chat_sessions_cache and msg_id:
824
+ chat_sessions_cache[session_key]['parent_message_id'] = msg_id
825
+ break
826
+ except json.JSONDecodeError:
827
+ full_content = response.text
828
+ break
829
+ else:
830
+ buffer = ""
831
+ for chunk in response.iter_content(chunk_size=None, decode_unicode=True):
832
+ if chunk:
833
+ buffer += chunk
834
+ while '\n' in buffer:
835
+ line, buffer = buffer.split('\n', 1)
836
+ line = line.strip()
837
+ if not line:
838
+ continue
839
+ if line.startswith('data: '):
840
+ line = line[6:]
841
+ if line == '[DONE]':
842
+ continue
843
+ content, msg_id, packet_type = parse_onyx_stream_chunk(line)
844
+ if msg_id:
845
+ last_message_id = msg_id
846
+ if packet_type == 'stop':
847
+ break
848
+ if content and packet_type in ['content', 'legacy', 'raw', 'error']:
849
+ full_content += content
850
+
851
+ if session_key in chat_sessions_cache and last_message_id:
852
+ chat_sessions_cache[session_key]['parent_message_id'] = last_message_id
853
+ break
854
+
855
+ except requests.exceptions.RequestException as e:
856
+ print(f"Anthropic request error: {e}")
857
+ continue
858
+
859
+ if not full_content:
860
+ return {
861
+ "type": "error",
862
+ "error": {
863
+ "type": "api_error",
864
+ "message": "No response from Onyx API"
865
+ }
866
+ }, 500
867
+
868
+ response_data = {
869
+ "id": f"msg_{uuid.uuid4().hex[:24]}",
870
+ "type": "message",
871
+ "role": "assistant",
872
+ "content": [{"type": "text", "text": full_content}],
873
+ "model": model,
874
+ "stop_reason": "end_turn",
875
+ "stop_sequence": None,
876
+ "usage": {
877
+ "input_tokens": 0,
878
+ "output_tokens": 0
879
+ }
880
+ }
881
+
882
+ return response_data, 200
883
+
884
+
885
+ @app.route('/v1/messages', methods=['POST'])
886
+ def anthropic_messages():
887
+ """Anthropic Messages API compatible endpoint — used by Claude Code"""
888
+
889
+ try:
890
+ data = request.json
891
+ print(f"[Anthropic] Received request: {json.dumps(data, indent=2)[:500]}")
892
+ except Exception as e:
893
+ return jsonify({
894
+ "type": "error",
895
+ "error": {"type": "invalid_request_error", "message": f"Invalid JSON: {e}"}
896
+ }), 400
897
+
898
+ # Extract Anthropic parameters
899
+ model = data.get('model', 'claude-opus-4-6')
900
+ messages = data.get('messages', [])
901
+ system_prompt = data.get('system', '')
902
+ stream = data.get('stream', False)
903
+ temperature = data.get('temperature', 0.7)
904
+ max_tokens = data.get('max_tokens', 4096)
905
+ tools = data.get('tools', None)
906
+
907
+ session_key = f"anthropic_{model}"
908
+
909
+ if not messages:
910
+ return jsonify({
911
+ "type": "error",
912
+ "error": {"type": "invalid_request_error", "message": "messages is required"}
913
+ }), 400
914
+
915
+ # Parse model — Anthropic sends bare model names like 'claude-opus-4-6'
916
+ # We need to add 'anthropic/' prefix if not present
917
+ if '/' not in model:
918
+ full_model = f"anthropic/{model}"
919
+ else:
920
+ full_model = model
921
+
922
+ model_provider, model_version = parse_model_string(full_model)
923
+ model_provider = normalize_provider_name(model_provider)
924
+ print(f"[Anthropic] Provider: {model_provider}, Version: {model_version}")
925
+
926
+ # Get or create session
927
+ session_info = get_or_create_session(session_key)
928
+ if not session_info:
929
+ return jsonify({
930
+ "type": "error",
931
+ "error": {"type": "api_error", "message": "Failed to create chat session"}
932
+ }), 500
933
+
934
+ # Build Onyx payload
935
+ payload = build_anthropic_payload_from_messages(
936
+ messages=messages,
937
+ system_prompt=system_prompt,
938
+ model_provider=model_provider,
939
+ model_version=model_version,
940
+ temperature=temperature,
941
+ chat_session_id=session_info['session_id'],
942
+ parent_message_id=session_info.get('parent_message_id'),
943
+ stream=stream,
944
+ tools=tools
945
+ )
946
+
947
+ if stream:
948
+ return Response(
949
+ generate_anthropic_stream_events(payload, model, session_key),
950
+ content_type='text/event-stream',
951
+ headers={
952
+ 'Cache-Control': 'no-cache',
953
+ 'Connection': 'keep-alive',
954
+ 'X-Accel-Buffering': 'no'
955
+ }
956
+ )
957
+ else:
958
+ response_data, status_code = collect_anthropic_full_response(payload, model, session_key)
959
+ return jsonify(response_data), status_code
960
+
961
+
962
  @app.route('/', methods=['GET'])
963
  def root():
964
  """Root endpoint with API info"""
965
  return jsonify({
966
+ "name": "OpenAI + Anthropic",
967
+ "version": "2.0.0",
968
  "endpoints": {
969
+ "chat_completions": "/v1/chat/completions (OpenAI format)",
970
+ "messages": "/v1/messages (Anthropic format)",
971
  "models": "/v1/models",
972
  "sessions": "/v1/sessions",
973
  "health": "/health",
974
+ "debug": "/debug"
975
  },
976
+ "model_format": "provider/model_version (e.g., openai/gpt-4, anthropic/claude-opus-4-6)"
977
  })
978
 
979