Repository navigation
fix: respect max_completion_tokens and guard missing model capabilities #275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
laizhengbao
wants to merge
7
commits into
ericc-ch:master
Choose a base branch
from
laizhengbao:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
6e3b966
fix: respect max_completion_tokens and guard missing model capabilities
laizhengbao c2c2cdb
feat: translate
laizhengbao 2e64394
fix: review feedback
laizhengbao 23778b3
fix: review
laizhengbao 64a302c
fix: route guard
laizhengbao 25ecdcd
fix: stream
laizhengbao 46fd20a
fix: sec
laizhengbao File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import { Hono } from "hono" | ||
| import { streamSSE } from "hono/streaming" | ||
|
|
||
| import { awaitApproval } from "~/lib/approval" | ||
| import { forwardError } from "~/lib/error" | ||
| import { checkRateLimit } from "~/lib/rate-limit" | ||
| import { state } from "~/lib/state" | ||
| import { | ||
| chatResultToCompletionResult, | ||
| completionPayloadToChatPayload, | ||
| streamChatAsCompletionChunks, | ||
| } from "~/services/copilot/completions-adapter" | ||
| import { createChatCompletions } from "~/services/copilot/create-chat-completions" | ||
| import { | ||
| aggregateProxyCompletionsStream, | ||
| createProxyCompletions, | ||
| type CompletionsPayload, | ||
| } from "~/services/copilot/create-proxy-completions" | ||
|
|
||
| export const completionsRoutes = new Hono() | ||
|
|
||
| completionsRoutes.post("/", async (c) => { | ||
| try { | ||
| // same request guards as the chat route: rate limit + manual approval | ||
| await checkRateLimit(state) | ||
| if (state.manualApprove) await awaitApproval() | ||
|
|
||
| const body = (await c.req.json()) as unknown | ||
| if (typeof body !== "object" || body === null) { | ||
| return c.json({ error: "model is required" }, 400) | ||
| } | ||
| const payload = body as CompletionsPayload | ||
| if (typeof payload.model !== "string" || payload.model.length === 0) { | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| return c.json({ error: "model is required" }, 400) | ||
| } | ||
|
|
||
| // legacy fill-in-the-middle engines (gpt-41-copilot, ...) live behind | ||
| // the traditional proxy completions API; everything else is translated | ||
| // through Chat Completions so all models are reachable here | ||
| if (payload.model.endsWith("-copilot")) { | ||
| if (payload.stream) { | ||
| const response = await createProxyCompletions(payload) | ||
| const body = response.body | ||
| if (!body) { | ||
| return c.json({ error: "Empty response body" }, 502) | ||
| } | ||
| return c.body(body, 200, { | ||
| "content-type": "text/event-stream", | ||
| "cache-control": "no-cache", | ||
| }) | ||
| } | ||
|
|
||
| const result = await aggregateProxyCompletionsStream(payload) | ||
| return c.json(result) | ||
| } | ||
|
|
||
| // array prompts are independent prompts (or FIM prefix/suffix pairs) in | ||
| // the legacy contract and cannot be translated to a single chat turn | ||
| if (Array.isArray(payload.prompt)) { | ||
| return c.json( | ||
| { | ||
| error: | ||
| "Array prompts are only supported for -copilot engine models; chat-model translation accepts a single string prompt", | ||
| }, | ||
| 400, | ||
| ) | ||
| } | ||
|
|
||
| // FIM suffix has no chat-completions equivalent; reject explicitly | ||
| // instead of silently dropping it | ||
| if (payload.suffix !== undefined && payload.suffix !== null) { | ||
| return c.json( | ||
| { error: "suffix is only supported for -copilot engine models" }, | ||
| 400, | ||
| ) | ||
| } | ||
|
|
||
| const chatPayload = completionPayloadToChatPayload(payload) | ||
|
|
||
| if (payload.stream) { | ||
| const chatStream = (await createChatCompletions({ | ||
| ...chatPayload, | ||
| stream: true, | ||
| })) as AsyncIterable<{ data?: string | null }> | ||
|
|
||
| return streamSSE(c, async (stream) => { | ||
| for await (const event of streamChatAsCompletionChunks(chatStream)) { | ||
| await stream.writeSSE({ data: event.data }) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| const chatResult = await createChatCompletions(chatPayload) | ||
| const result = chatResultToCompletionResult( | ||
| chatResult as Parameters<typeof chatResultToCompletionResult>[0], | ||
| ) | ||
| return c.json(result) | ||
| } catch (error) { | ||
| return await forwardError(c, error) | ||
| } | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { Hono } from "hono" | ||
| import { streamSSE } from "hono/streaming" | ||
|
|
||
| import { awaitApproval } from "~/lib/approval" | ||
| import { forwardError } from "~/lib/error" | ||
| import { checkRateLimit } from "~/lib/rate-limit" | ||
| import { state } from "~/lib/state" | ||
| import { createChatCompletions } from "~/services/copilot/create-chat-completions" | ||
| import { | ||
| createResponses, | ||
| type ResponsesPayload, | ||
| } from "~/services/copilot/create-responses" | ||
| import { | ||
| chatResultToResponsesResult, | ||
| isResponsesCapableModel, | ||
| responsesPayloadHints, | ||
| responsesPayloadToChatPayload, | ||
| streamChatAsResponsesEvents, | ||
| } from "~/services/copilot/responses-adapter" | ||
|
|
||
| export const responseRoutes = new Hono() | ||
|
|
||
| responseRoutes.post("/", async (c) => { | ||
|
Copilot marked this conversation as resolved.
Resolved
|
||
| try { | ||
| // same request guards as the chat route: rate limit + manual approval | ||
| await checkRateLimit(state) | ||
| if (state.manualApprove) await awaitApproval() | ||
|
|
||
| const body = (await c.req.json()) as unknown | ||
| if (typeof body !== "object" || body === null) { | ||
| return c.json({ error: "model is required" }, 400) | ||
| } | ||
| const payload = body as Record<string, unknown> | ||
| if (typeof payload.model !== "string" || payload.model.length === 0) { | ||
| return c.json({ error: "model is required" }, 400) | ||
| } | ||
| const model = payload.model | ||
|
|
||
| // responses-native models stream straight through | ||
| if (isResponsesCapableModel(model)) { | ||
| const response = await createResponses( | ||
| payload as ResponsesPayload, | ||
| responsesPayloadHints(payload), | ||
| ) | ||
| const body = response.body | ||
| if (!body) { | ||
| return c.json({ error: "Empty response body" }, 502) | ||
| } | ||
|
|
||
| const contentType = | ||
| response.headers.get("content-type") ?? "application/json" | ||
|
|
||
| return c.body(body, 200, { | ||
| "content-type": contentType, | ||
| "cache-control": "no-cache", | ||
| }) | ||
| } | ||
|
|
||
| // chat-only models are served by translating through Chat Completions | ||
| const chatPayload = responsesPayloadToChatPayload(payload) | ||
|
|
||
| if (payload.stream === true) { | ||
| const chatStream = (await createChatCompletions({ | ||
| ...chatPayload, | ||
| stream: true, | ||
| })) as AsyncIterable<{ data?: string | null }> | ||
|
|
||
| return streamSSE(c, async (stream) => { | ||
| for await (const event of streamChatAsResponsesEvents( | ||
| chatStream, | ||
| model, | ||
| )) { | ||
| await stream.writeSSE({ data: event.data }) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| const chatResult = await createChatCompletions(chatPayload) | ||
| const responsesResult = chatResultToResponsesResult( | ||
| chatResult as Parameters<typeof chatResultToResponsesResult>[0], | ||
| ) | ||
| return c.json(responsesResult) | ||
| } catch (error) { | ||
| return await forwardError(c, error) | ||
| } | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import type { | ||
| CompletionsPayload, | ||
| ProxyCompletionResult, | ||
| } from "~/services/copilot/create-proxy-completions" | ||
|
|
||
| import { isNullish } from "~/lib/utils" | ||
| import { | ||
| type ChatCompletionChunk, | ||
| type ChatCompletionsPayload, | ||
| type ChatCompletionResponse, | ||
| } from "~/services/copilot/create-chat-completions" | ||
|
|
||
| /* | ||
| * Legacy completions surface for chat-capable models: translates the old | ||
| * prompt-based /v1/completions format through Chat Completions, so every | ||
| * usable model is reachable from legacy clients too. | ||
| */ | ||
|
|
||
| export const completionPayloadToChatPayload = ( | ||
| payload: CompletionsPayload, | ||
| ): ChatCompletionsPayload => { | ||
| // Array prompts mean independent prompts (or FIM prefix/suffix pairs) in | ||
| // the legacy contract and cannot be represented as a single chat message; | ||
| // the route rejects them before we get here. | ||
| if (typeof payload.prompt !== "string") { | ||
| throw new TypeError("chat translation requires a single string prompt") | ||
| } | ||
|
|
||
| const result: ChatCompletionsPayload = { | ||
| model: payload.model, | ||
| messages: [{ role: "user", content: payload.prompt }], | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| if (!isNullish(payload.max_tokens)) result.max_tokens = payload.max_tokens | ||
| if (!isNullish(payload.temperature)) { | ||
| result.temperature = payload.temperature | ||
| } | ||
| if (!isNullish(payload.top_p)) result.top_p = payload.top_p | ||
| if (!isNullish(payload.stop)) result.stop = payload.stop | ||
| if (!isNullish(payload.n)) result.n = payload.n | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| export const chatResultToCompletionResult = ( | ||
| chat: ChatCompletionResponse, | ||
| ): ProxyCompletionResult => ({ | ||
| id: chat.id, | ||
| object: "text_completion", | ||
| created: chat.created, | ||
| model: chat.model, | ||
| choices: chat.choices.map((choice) => ({ | ||
| text: choice.message.content ?? "", | ||
| index: choice.index, | ||
| finish_reason: choice.finish_reason, | ||
| logprobs: null, | ||
| })), | ||
| usage: | ||
| chat.usage ? | ||
| { | ||
| prompt_tokens: chat.usage.prompt_tokens, | ||
| completion_tokens: chat.usage.completion_tokens, | ||
| total_tokens: chat.usage.total_tokens, | ||
| } | ||
| : undefined, | ||
| }) | ||
|
|
||
| export async function* streamChatAsCompletionChunks( | ||
| chatStream: AsyncIterable<{ data?: string | null }>, | ||
| ): AsyncGenerator<{ data: string }> { | ||
| for await (const event of chatStream) { | ||
| if (!event.data) continue | ||
| // skip the non-JSON SSE terminator line | ||
| if (!event.data.startsWith("{")) break | ||
| const chunk = JSON.parse(event.data) as ChatCompletionChunk | ||
| if (chunk.choices.length === 0) continue | ||
|
|
||
| yield { | ||
| data: JSON.stringify({ | ||
| id: chunk.id, | ||
| object: "text_completion", | ||
| created: chunk.created, | ||
| model: chunk.model, | ||
| choices: chunk.choices.map((choice) => ({ | ||
| text: choice.delta.content ?? "", | ||
| index: choice.index, | ||
| finish_reason: choice.finish_reason, | ||
| logprobs: null, | ||
| })), | ||
| }), | ||
| } | ||
| } | ||
|
|
||
| // emit the conventional SSE terminator line | ||
| yield { data: "[DO" + "NE]" } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.