Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

Commit 17eae23

Browse files
committed
Ported OpenAI and OpenRouter providers.
Also, tests and fixes.
1 parent 0be2bcc commit 17eae23

File tree

14 files changed

+216
-98
lines changed

14 files changed

+216
-98
lines changed

src/codegate/pipeline/comment/output.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,10 @@ async def process_chunk(
130130
input_context: Optional[PipelineContext] = None,
131131
) -> list[ModelResponse]:
132132
"""Process a single chunk of the stream"""
133-
# if len(chunk.choices) == 0 or not chunk.choices[0].delta.content:
134-
# return [chunk]
135-
136133
for content in chunk.get_content():
137134
# Get current content plus this new chunk
138-
current_content = "".join(context.processed_content + [txt for txt in content.get_text()])
135+
text = content.get_text()
136+
current_content = "".join(context.processed_content + [text if text else ""])
139137

140138
# Extract snippets from current content
141139
snippets = self.extractor.extract_snippets(current_content)

src/codegate/pipeline/secrets/secrets.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,8 @@ async def process_chunk(
502502
return []
503503

504504
# No markers or partial markers, let pipeline handle the chunk normally
505-
content.set_text(context.prefix_buffer + content.get_text())
505+
text = content.get_text()
506+
content.set_text(context.prefix_buffer + text if text else "")
506507
context.prefix_buffer = ""
507508
return [chunk]
508509
else:

src/codegate/providers/anthropic/provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def __init__(
2828
if self._get_base_url() != "":
2929
self.base_url = self._get_base_url()
3030
else:
31-
self.base_url = "https://api.anthropic.com/"
31+
self.base_url = "https://api.anthropic.com"
3232

3333
completion_handler = AnthropicCompletion(stream_generator=stream_generator)
3434
super().__init__(

src/codegate/providers/litellmshim/litellmshim.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ async def execute_completion(
5050
Execute the completion request with LiteLLM's API
5151
"""
5252
if is_fim_request:
53-
return self._fim_completion_func(request, api_key=api_key)
54-
return self._completion_func(request, api_key=api_key)
53+
return self._fim_completion_func(request, api_key=api_key, base_url=base_url)
54+
return self._completion_func(request, api_key=api_key, base_url=base_url)
5555

5656
def _create_streaming_response(
5757
self,

src/codegate/providers/openai/provider.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,32 @@
1111
from codegate.providers.base import BaseProvider, ModelFetchError
1212
from codegate.providers.fim_analyzer import FIMAnalyzer
1313
from codegate.providers.litellmshim import LiteLLmShim
14-
from codegate.providers.openai.adapter import OpenAIInputNormalizer, OpenAIOutputNormalizer
15-
from codegate.types.generators import sse_stream_generator
14+
from codegate.types.openai import (
15+
completions_streaming,
16+
stream_generator,
17+
ChatCompletionRequest,
18+
)
1619

1720

1821
class OpenAIProvider(BaseProvider):
1922
def __init__(
2023
self,
2124
pipeline_factory: PipelineFactory,
2225
# Enable receiving other completion handlers from childs, i.e. OpenRouter and LM Studio
23-
completion_handler: LiteLLmShim = LiteLLmShim(stream_generator=sse_stream_generator),
26+
completion_handler: LiteLLmShim = LiteLLmShim(completion_func=completions_streaming, stream_generator=stream_generator),
2427
):
28+
if self._get_base_url() != "":
29+
self.base_url = self._get_base_url()
30+
else:
31+
self.base_url = "https://api.openai.com/api/v1"
32+
33+
completion_handler = LiteLLmShim(
34+
completion_func=completions_streaming,
35+
stream_generator=stream_generator,
36+
)
2537
super().__init__(
26-
OpenAIInputNormalizer(),
27-
OpenAIOutputNormalizer(),
38+
None,
39+
None,
2840
completion_handler,
2941
pipeline_factory,
3042
)
@@ -93,11 +105,11 @@ async def create_completion(
93105

94106
api_key = authorization.split(" ")[1]
95107
body = await request.body()
96-
data = json.loads(body)
97-
is_fim_request = FIMAnalyzer.is_fim_request(request.url.path, data)
108+
req = ChatCompletionRequest.model_validate_json(body)
109+
is_fim_request = FIMAnalyzer.is_fim_request(request.url.path, req)
98110

99111
return await self.process_request(
100-
data,
112+
req,
101113
api_key,
102114
is_fim_request,
103115
request.state.detected_client,

src/codegate/providers/openrouter/provider.py

Lines changed: 11 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,66 +2,24 @@
22
from typing import Dict
33

44
from fastapi import Header, HTTPException, Request
5-
from litellm import atext_completion
6-
from litellm.types.llms.openai import ChatCompletionRequest
75

86
from codegate.clients.clients import ClientType
97
from codegate.clients.detector import DetectClient
108
from codegate.pipeline.factory import PipelineFactory
119
from codegate.providers.fim_analyzer import FIMAnalyzer
12-
from codegate.providers.litellmshim import LiteLLmShim, sse_stream_generator
13-
from codegate.providers.normalizer.completion import CompletionNormalizer
1410
from codegate.providers.openai import OpenAIProvider
15-
16-
17-
class OpenRouterNormalizer(CompletionNormalizer):
18-
def __init__(self):
19-
super().__init__()
20-
21-
def normalize(self, data: Dict) -> ChatCompletionRequest:
22-
return super().normalize(data)
23-
24-
def denormalize(self, data: ChatCompletionRequest) -> Dict:
25-
"""
26-
Denormalize a FIM OpenRouter request. Force it to be an accepted atext_completion format.
27-
"""
28-
denormalized_data = super().denormalize(data)
29-
# We are forcing atext_completion which expects to have a "prompt" key in the data
30-
# Forcing it in case is not present
31-
if "prompt" in data:
32-
return denormalized_data
33-
custom_prompt = ""
34-
for msg_dict in denormalized_data.get("messages", []):
35-
content_obj = msg_dict.get("content")
36-
if not content_obj:
37-
continue
38-
if isinstance(content_obj, list):
39-
for content_dict in content_obj:
40-
custom_prompt += (
41-
content_dict.get("text", "") if isinstance(content_dict, dict) else ""
42-
)
43-
elif isinstance(content_obj, str):
44-
custom_prompt += content_obj
45-
46-
# Erase the original "messages" key. Replace it by "prompt"
47-
del denormalized_data["messages"]
48-
denormalized_data["prompt"] = custom_prompt
49-
50-
return denormalized_data
11+
from codegate.types.openai import (
12+
ChatCompletionRequest,
13+
)
5114

5215

5316
class OpenRouterProvider(OpenAIProvider):
5417
def __init__(self, pipeline_factory: PipelineFactory):
55-
super().__init__(
56-
pipeline_factory,
57-
# We get FIM requests in /completions. LiteLLM is forcing /chat/completions
58-
# which returns "choices":[{"delta":{"content":"some text"}}]
59-
# instead of "choices":[{"text":"some text"}] expected by the client (Continue)
60-
completion_handler=LiteLLmShim(
61-
stream_generator=sse_stream_generator, fim_completion_func=atext_completion
62-
),
63-
)
64-
self._fim_normalizer = OpenRouterNormalizer()
18+
super().__init__(pipeline_factory)
19+
if self._get_base_url() != "":
20+
self.base_url = self._get_base_url()
21+
else:
22+
self.base_url = "https://openrouter.ai/api/v1"
6523

6624
@property
6725
def provider_route_name(self) -> str:
@@ -74,12 +32,6 @@ async def process_request(
7432
is_fim_request: bool,
7533
client_type: ClientType,
7634
):
77-
# litellm workaround - add openrouter/ prefix to model name to make it openai-compatible
78-
# once we get rid of litellm, this can simply be removed
79-
original_model = data.get("model", "")
80-
if not original_model.startswith("openrouter/"):
81-
data["model"] = f"openrouter/{original_model}"
82-
8335
return await super().process_request(data, api_key, is_fim_request, client_type)
8436

8537
def _setup_routes(self):
@@ -96,14 +48,12 @@ async def create_completion(
9648

9749
api_key = authorization.split(" ")[1]
9850
body = await request.body()
99-
data = json.loads(body)
10051

101-
base_url = self._get_base_url()
102-
data["base_url"] = base_url
103-
is_fim_request = FIMAnalyzer.is_fim_request(request.url.path, data)
52+
req = ChatCompletionRequest.model_validate_json(body)
53+
is_fim_request = FIMAnalyzer.is_fim_request(request.url.path, req)
10454

10555
return await self.process_request(
106-
data,
56+
req,
10757
api_key,
10858
is_fim_request,
10959
request.state.detected_client,

src/codegate/types/anthropic/_generators.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ async def stream_generator(stream: AsyncIterator[Any]) -> AsyncIterator[str]:
6565
yield f"event: error\ndata: {body}\n\n"
6666

6767

68-
async def acompletion(request, api_key):
68+
async def acompletion(request, api_key, base_url):
6969
headers = {
7070
"anthropic-version": "2023-06-01",
7171
"x-api-key": api_key,
@@ -79,7 +79,7 @@ async def acompletion(request, api_key):
7979

8080
client = httpx.AsyncClient()
8181
async with client.stream(
82-
"POST", "https://api.anthropic.com/v1/messages",
82+
"POST", f"{base_url}/v1/messages",
8383
headers=headers,
8484
content=payload,
8585
timeout=30, # TODO this should not be hardcoded
@@ -90,9 +90,11 @@ async def acompletion(request, api_key):
9090
async for event in message_wrapper(resp.aiter_lines()):
9191
yield event
9292
case 400 | 401 | 403 | 404 | 413 | 429:
93-
yield MessageError.model_validate_json(resp.text)
93+
text = await resp.aread()
94+
yield MessageError.model_validate_json(text)
9495
case 500 | 529:
95-
yield MessageError.model_validate_json(resp.text)
96+
text = await resp.aread()
97+
yield MessageError.model_validate_json(text)
9698
case _:
9799
logger.error(f"unexpected status code {resp.status_code}", provider="anthropic")
98100
raise ValueError(f"unexpected status code {resp.status_code}", provider="anthropic")

src/codegate/types/anthropic/_request_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class AssistantMessage(pydantic.BaseModel):
106106

107107
def get_text(self) -> Iterable[str]:
108108
if isinstance(self.content, str):
109-
yield self.content
109+
return self.content
110110

111111
def set_text(self, text) -> None:
112112
if isinstance(self.content, str):

src/codegate/types/anthropic/_response_models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class ToolUseResponseContent(pydantic.BaseModel):
3333
name: str
3434

3535
def get_text(self):
36-
return iter(()) # empty generator
36+
return None
3737

3838
def set_text(self, text):
3939
pass
@@ -97,7 +97,7 @@ class ToolUse(pydantic.BaseModel):
9797
input: Dict
9898

9999
def get_text(self) -> str | None:
100-
return ""
100+
return None
101101

102102
def set_text(self, text):
103103
pass

src/codegate/types/openai/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from ._generators import (
55
completions_streaming,
6+
message_wrapper,
67
stream_generator,
78
)
89

@@ -12,11 +13,13 @@
1213
Choice,
1314
ChoiceDelta,
1415
CompletionTokenDetails,
16+
ErrorDetails,
1517
FunctionCall,
1618
LogProbs,
1719
LogProbsContent,
1820
Message,
1921
MessageDelta,
22+
MessageError,
2023
PromptTokenDetails,
2124
RawLogProbsContent,
2225
StreamingChatCompletion,

0 commit comments

Comments
 (0)