@gajae-code/ai 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2644 -0
- package/README.md +1181 -0
- package/dist/types/api-registry.d.ts +30 -0
- package/dist/types/auth-broker/client.d.ts +66 -0
- package/dist/types/auth-broker/index.d.ts +5 -0
- package/dist/types/auth-broker/refresher.d.ts +25 -0
- package/dist/types/auth-broker/remote-store.d.ts +96 -0
- package/dist/types/auth-broker/server.d.ts +32 -0
- package/dist/types/auth-broker/types.d.ts +105 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +412 -0
- package/dist/types/auth-gateway/http.d.ts +39 -0
- package/dist/types/auth-gateway/index.d.ts +3 -0
- package/dist/types/auth-gateway/server.d.ts +17 -0
- package/dist/types/auth-gateway/types.d.ts +115 -0
- package/dist/types/auth-storage.d.ts +641 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/index.d.ts +49 -0
- package/dist/types/model-cache.d.ts +17 -0
- package/dist/types/model-manager.d.ts +62 -0
- package/dist/types/model-thinking.d.ts +71 -0
- package/dist/types/models.d.ts +12 -0
- package/dist/types/provider-details.d.ts +24 -0
- package/dist/types/provider-models/bundled-references.d.ts +4 -0
- package/dist/types/provider-models/descriptors.d.ts +48 -0
- package/dist/types/provider-models/google.d.ts +20 -0
- package/dist/types/provider-models/index.d.ts +5 -0
- package/dist/types/provider-models/ollama.d.ts +7 -0
- package/dist/types/provider-models/openai-compat.d.ts +237 -0
- package/dist/types/provider-models/special.d.ts +16 -0
- package/dist/types/providers/amazon-bedrock.d.ts +36 -0
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
- package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
- package/dist/types/providers/anthropic.d.ts +188 -0
- package/dist/types/providers/aws-credentials.d.ts +43 -0
- package/dist/types/providers/aws-eventstream.d.ts +38 -0
- package/dist/types/providers/aws-sigv4.d.ts +55 -0
- package/dist/types/providers/azure-openai-responses.d.ts +15 -0
- package/dist/types/providers/cursor/gen/agent_pb.d.ts +13022 -0
- package/dist/types/providers/cursor.d.ts +42 -0
- package/dist/types/providers/error-message.d.ts +27 -0
- package/dist/types/providers/github-copilot-headers.d.ts +40 -0
- package/dist/types/providers/gitlab-duo.d.ts +27 -0
- package/dist/types/providers/google-auth.d.ts +24 -0
- package/dist/types/providers/google-gemini-cli.d.ts +72 -0
- package/dist/types/providers/google-gemini-headers.d.ts +18 -0
- package/dist/types/providers/google-shared.d.ts +163 -0
- package/dist/types/providers/google-types.d.ts +138 -0
- package/dist/types/providers/google-vertex.d.ts +7 -0
- package/dist/types/providers/google.d.ts +4 -0
- package/dist/types/providers/grammar.d.ts +1 -0
- package/dist/types/providers/kimi.d.ts +27 -0
- package/dist/types/providers/mock.d.ts +175 -0
- package/dist/types/providers/ollama.d.ts +6 -0
- package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +814 -0
- package/dist/types/providers/openai-chat-server.d.ts +16 -0
- package/dist/types/providers/openai-codex/constants.d.ts +26 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +49 -0
- package/dist/types/providers/openai-codex/response-handler.d.ts +17 -0
- package/dist/types/providers/openai-codex-responses.d.ts +67 -0
- package/dist/types/providers/openai-completions-compat.d.ts +25 -0
- package/dist/types/providers/openai-completions.d.ts +33 -0
- package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
- package/dist/types/providers/openai-responses-server.d.ts +17 -0
- package/dist/types/providers/openai-responses-shared.d.ts +89 -0
- package/dist/types/providers/openai-responses.d.ts +32 -0
- package/dist/types/providers/pi-native-client.d.ts +13 -0
- package/dist/types/providers/pi-native-server.d.ts +68 -0
- package/dist/types/providers/register-builtins.d.ts +31 -0
- package/dist/types/providers/synthetic.d.ts +26 -0
- package/dist/types/providers/transform-messages.d.ts +12 -0
- package/dist/types/providers/vision-guard.d.ts +8 -0
- package/dist/types/rate-limit-utils.d.ts +19 -0
- package/dist/types/stream.d.ts +24 -0
- package/dist/types/types.d.ts +746 -0
- package/dist/types/usage/claude.d.ts +3 -0
- package/dist/types/usage/gemini.d.ts +2 -0
- package/dist/types/usage/github-copilot.d.ts +7 -0
- package/dist/types/usage/google-antigravity.d.ts +2 -0
- package/dist/types/usage/kimi.d.ts +2 -0
- package/dist/types/usage/minimax-code.d.ts +2 -0
- package/dist/types/usage/openai-codex.d.ts +3 -0
- package/dist/types/usage/shared.d.ts +1 -0
- package/dist/types/usage/zai.d.ts +2 -0
- package/dist/types/usage.d.ts +258 -0
- package/dist/types/utils/abort.d.ts +19 -0
- package/dist/types/utils/anthropic-auth.d.ts +31 -0
- package/dist/types/utils/discovery/antigravity.d.ts +61 -0
- package/dist/types/utils/discovery/codex.d.ts +38 -0
- package/dist/types/utils/discovery/cursor.d.ts +23 -0
- package/dist/types/utils/discovery/gemini.d.ts +25 -0
- package/dist/types/utils/discovery/index.d.ts +4 -0
- package/dist/types/utils/discovery/openai-compatible.d.ts +72 -0
- package/dist/types/utils/event-stream.d.ts +28 -0
- package/dist/types/utils/fireworks-model-id.d.ts +10 -0
- package/dist/types/utils/foundry.d.ts +1 -0
- package/dist/types/utils/h2-fetch.d.ts +22 -0
- package/dist/types/utils/http-inspector.d.ts +31 -0
- package/dist/types/utils/idle-iterator.d.ts +67 -0
- package/dist/types/utils/json-parse.d.ts +10 -0
- package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +18 -0
- package/dist/types/utils/oauth/anthropic.d.ts +22 -0
- package/dist/types/utils/oauth/api-key-login.d.ts +35 -0
- package/dist/types/utils/oauth/api-key-validation.d.ts +27 -0
- package/dist/types/utils/oauth/callback-server.d.ts +57 -0
- package/dist/types/utils/oauth/cerebras.d.ts +1 -0
- package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
- package/dist/types/utils/oauth/cursor.d.ts +15 -0
- package/dist/types/utils/oauth/deepseek.d.ts +10 -0
- package/dist/types/utils/oauth/firepass.d.ts +1 -0
- package/dist/types/utils/oauth/fireworks.d.ts +1 -0
- package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
- package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
- package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
- package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
- package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
- package/dist/types/utils/oauth/huggingface.d.ts +19 -0
- package/dist/types/utils/oauth/index.d.ts +38 -0
- package/dist/types/utils/oauth/kagi.d.ts +17 -0
- package/dist/types/utils/oauth/kilo.d.ts +5 -0
- package/dist/types/utils/oauth/kimi.d.ts +21 -0
- package/dist/types/utils/oauth/litellm.d.ts +18 -0
- package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
- package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
- package/dist/types/utils/oauth/moonshot.d.ts +1 -0
- package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
- package/dist/types/utils/oauth/nvidia.d.ts +18 -0
- package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
- package/dist/types/utils/oauth/ollama.d.ts +18 -0
- package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
- package/dist/types/utils/oauth/opencode.d.ts +18 -0
- package/dist/types/utils/oauth/parallel.d.ts +17 -0
- package/dist/types/utils/oauth/perplexity.d.ts +9 -0
- package/dist/types/utils/oauth/pkce.d.ts +8 -0
- package/dist/types/utils/oauth/qianfan.d.ts +17 -0
- package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
- package/dist/types/utils/oauth/synthetic.d.ts +1 -0
- package/dist/types/utils/oauth/tavily.d.ts +17 -0
- package/dist/types/utils/oauth/together.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +44 -0
- package/dist/types/utils/oauth/venice.d.ts +18 -0
- package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
- package/dist/types/utils/oauth/vllm.d.ts +16 -0
- package/dist/types/utils/oauth/xiaomi.d.ts +19 -0
- package/dist/types/utils/oauth/zai.d.ts +18 -0
- package/dist/types/utils/oauth/zenmux.d.ts +1 -0
- package/dist/types/utils/overflow.d.ts +54 -0
- package/dist/types/utils/parse-bind.d.ts +23 -0
- package/dist/types/utils/provider-response.d.ts +3 -0
- package/dist/types/utils/retry-after.d.ts +3 -0
- package/dist/types/utils/retry.d.ts +26 -0
- package/dist/types/utils/schema/adapt.d.ts +24 -0
- package/dist/types/utils/schema/compatibility.d.ts +30 -0
- package/dist/types/utils/schema/dereference.d.ts +11 -0
- package/dist/types/utils/schema/draft.d.ts +10 -0
- package/dist/types/utils/schema/equality.d.ts +4 -0
- package/dist/types/utils/schema/fields.d.ts +49 -0
- package/dist/types/utils/schema/index.d.ts +13 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
- package/dist/types/utils/schema/meta-validator.d.ts +2 -0
- package/dist/types/utils/schema/normalize.d.ts +93 -0
- package/dist/types/utils/schema/spill.d.ts +8 -0
- package/dist/types/utils/schema/stamps.d.ts +25 -0
- package/dist/types/utils/schema/types.d.ts +4 -0
- package/dist/types/utils/schema/wire.d.ts +54 -0
- package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
- package/dist/types/utils/sse-debug.d.ts +10 -0
- package/dist/types/utils/tool-call-healing.d.ts +71 -0
- package/dist/types/utils/tool-choice.d.ts +50 -0
- package/dist/types/utils/validation.d.ts +17 -0
- package/dist/types/utils.d.ts +28 -0
- package/package.json +146 -0
- package/src/api-registry.ts +96 -0
- package/src/auth-broker/client.ts +358 -0
- package/src/auth-broker/index.ts +5 -0
- package/src/auth-broker/refresher.ts +127 -0
- package/src/auth-broker/remote-store.ts +623 -0
- package/src/auth-broker/server.ts +644 -0
- package/src/auth-broker/types.ts +127 -0
- package/src/auth-broker/wire-schemas.ts +200 -0
- package/src/auth-gateway/http.ts +194 -0
- package/src/auth-gateway/index.ts +3 -0
- package/src/auth-gateway/server.ts +717 -0
- package/src/auth-gateway/types.ts +134 -0
- package/src/auth-storage.ts +4104 -0
- package/src/cli.ts +262 -0
- package/src/index.ts +54 -0
- package/src/model-cache.ts +129 -0
- package/src/model-manager.ts +450 -0
- package/src/model-thinking.ts +691 -0
- package/src/models.json +73853 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +56 -0
- package/src/prompts/turn-aborted-guidance.md +4 -0
- package/src/provider-details.ts +90 -0
- package/src/provider-models/bundled-references.ts +38 -0
- package/src/provider-models/descriptors.ts +308 -0
- package/src/provider-models/google.ts +91 -0
- package/src/provider-models/index.ts +5 -0
- package/src/provider-models/ollama.ts +153 -0
- package/src/provider-models/openai-compat.ts +2275 -0
- package/src/provider-models/special.ts +67 -0
- package/src/providers/amazon-bedrock.ts +849 -0
- package/src/providers/anthropic-messages-server-schema.ts +229 -0
- package/src/providers/anthropic-messages-server.ts +677 -0
- package/src/providers/anthropic.ts +2696 -0
- package/src/providers/aws-credentials.ts +501 -0
- package/src/providers/aws-eventstream.ts +185 -0
- package/src/providers/aws-sigv4.ts +218 -0
- package/src/providers/azure-openai-responses.ts +337 -0
- package/src/providers/cursor/gen/agent_pb.ts +15274 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +2561 -0
- package/src/providers/error-message.ts +21 -0
- package/src/providers/github-copilot-headers.ts +140 -0
- package/src/providers/gitlab-duo.ts +372 -0
- package/src/providers/google-auth.ts +252 -0
- package/src/providers/google-gemini-cli.ts +795 -0
- package/src/providers/google-gemini-headers.ts +41 -0
- package/src/providers/google-shared.ts +902 -0
- package/src/providers/google-types.ts +167 -0
- package/src/providers/google-vertex.ts +88 -0
- package/src/providers/google.ts +41 -0
- package/src/providers/grammar.ts +70 -0
- package/src/providers/kimi.ts +52 -0
- package/src/providers/mock.ts +500 -0
- package/src/providers/ollama.ts +544 -0
- package/src/providers/openai-anthropic-shim.ts +138 -0
- package/src/providers/openai-chat-server-schema.ts +243 -0
- package/src/providers/openai-chat-server.ts +628 -0
- package/src/providers/openai-codex/constants.ts +43 -0
- package/src/providers/openai-codex/request-transformer.ts +161 -0
- package/src/providers/openai-codex/response-handler.ts +81 -0
- package/src/providers/openai-codex-responses.ts +2598 -0
- package/src/providers/openai-completions-compat.ts +279 -0
- package/src/providers/openai-completions.ts +1853 -0
- package/src/providers/openai-responses-server-schema.ts +290 -0
- package/src/providers/openai-responses-server.ts +1183 -0
- package/src/providers/openai-responses-shared.ts +800 -0
- package/src/providers/openai-responses.ts +621 -0
- package/src/providers/pi-native-client.ts +228 -0
- package/src/providers/pi-native-server.ts +210 -0
- package/src/providers/register-builtins.ts +412 -0
- package/src/providers/synthetic.ts +50 -0
- package/src/providers/transform-messages.ts +309 -0
- package/src/providers/vision-guard.ts +31 -0
- package/src/rate-limit-utils.ts +84 -0
- package/src/stream.ts +895 -0
- package/src/types.ts +884 -0
- package/src/usage/claude.ts +431 -0
- package/src/usage/gemini.ts +250 -0
- package/src/usage/github-copilot.ts +421 -0
- package/src/usage/google-antigravity.ts +201 -0
- package/src/usage/kimi.ts +271 -0
- package/src/usage/minimax-code.ts +31 -0
- package/src/usage/openai-codex.ts +503 -0
- package/src/usage/shared.ts +10 -0
- package/src/usage/zai.ts +247 -0
- package/src/usage.ts +183 -0
- package/src/utils/abort.ts +51 -0
- package/src/utils/anthropic-auth.ts +87 -0
- package/src/utils/discovery/antigravity.ts +261 -0
- package/src/utils/discovery/codex.ts +371 -0
- package/src/utils/discovery/cursor.ts +306 -0
- package/src/utils/discovery/gemini.ts +248 -0
- package/src/utils/discovery/index.ts +4 -0
- package/src/utils/discovery/openai-compatible.ts +224 -0
- package/src/utils/event-stream.ts +142 -0
- package/src/utils/fireworks-model-id.ts +30 -0
- package/src/utils/foundry.ts +8 -0
- package/src/utils/h2-fetch.ts +60 -0
- package/src/utils/http-inspector.ts +176 -0
- package/src/utils/idle-iterator.ts +250 -0
- package/src/utils/json-parse.ts +148 -0
- package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
- package/src/utils/oauth/anthropic.ts +200 -0
- package/src/utils/oauth/api-key-login.ts +87 -0
- package/src/utils/oauth/api-key-validation.ts +92 -0
- package/src/utils/oauth/callback-server.ts +276 -0
- package/src/utils/oauth/cerebras.ts +16 -0
- package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
- package/src/utils/oauth/cursor.ts +157 -0
- package/src/utils/oauth/deepseek.ts +53 -0
- package/src/utils/oauth/firepass.ts +24 -0
- package/src/utils/oauth/fireworks.ts +15 -0
- package/src/utils/oauth/github-copilot.ts +362 -0
- package/src/utils/oauth/gitlab-duo.ts +123 -0
- package/src/utils/oauth/google-antigravity.ts +200 -0
- package/src/utils/oauth/google-gemini-cli.ts +256 -0
- package/src/utils/oauth/google-oauth-shared.ts +110 -0
- package/src/utils/oauth/huggingface.ts +62 -0
- package/src/utils/oauth/index.ts +444 -0
- package/src/utils/oauth/kagi.ts +47 -0
- package/src/utils/oauth/kilo.ts +87 -0
- package/src/utils/oauth/kimi.ts +254 -0
- package/src/utils/oauth/litellm.ts +47 -0
- package/src/utils/oauth/lm-studio.ts +38 -0
- package/src/utils/oauth/minimax-code.ts +78 -0
- package/src/utils/oauth/moonshot.ts +16 -0
- package/src/utils/oauth/nanogpt.ts +15 -0
- package/src/utils/oauth/nvidia.ts +70 -0
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/ollama-cloud.ts +28 -0
- package/src/utils/oauth/ollama.ts +47 -0
- package/src/utils/oauth/openai-codex.ts +299 -0
- package/src/utils/oauth/opencode.ts +49 -0
- package/src/utils/oauth/parallel.ts +46 -0
- package/src/utils/oauth/perplexity.ts +206 -0
- package/src/utils/oauth/pkce.ts +18 -0
- package/src/utils/oauth/qianfan.ts +58 -0
- package/src/utils/oauth/qwen-portal.ts +60 -0
- package/src/utils/oauth/synthetic.ts +16 -0
- package/src/utils/oauth/tavily.ts +46 -0
- package/src/utils/oauth/together.ts +16 -0
- package/src/utils/oauth/types.ts +94 -0
- package/src/utils/oauth/venice.ts +59 -0
- package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
- package/src/utils/oauth/vllm.ts +40 -0
- package/src/utils/oauth/xiaomi.ts +137 -0
- package/src/utils/oauth/zai.ts +60 -0
- package/src/utils/oauth/zenmux.ts +15 -0
- package/src/utils/overflow.ts +137 -0
- package/src/utils/parse-bind.ts +54 -0
- package/src/utils/provider-response.ts +30 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry.ts +54 -0
- package/src/utils/schema/CONSTRAINTS.md +164 -0
- package/src/utils/schema/adapt.ts +36 -0
- package/src/utils/schema/compatibility.ts +435 -0
- package/src/utils/schema/dereference.ts +98 -0
- package/src/utils/schema/draft.ts +341 -0
- package/src/utils/schema/equality.ts +97 -0
- package/src/utils/schema/fields.ts +190 -0
- package/src/utils/schema/index.ts +13 -0
- package/src/utils/schema/json-schema-validator.ts +577 -0
- package/src/utils/schema/meta-validator.ts +167 -0
- package/src/utils/schema/normalize.ts +1588 -0
- package/src/utils/schema/spill.ts +43 -0
- package/src/utils/schema/stamps.ts +97 -0
- package/src/utils/schema/types.ts +11 -0
- package/src/utils/schema/wire.ts +213 -0
- package/src/utils/schema/zod-decontaminate.ts +331 -0
- package/src/utils/sse-debug.ts +289 -0
- package/src/utils/tool-call-healing.ts +271 -0
- package/src/utils/tool-choice.ts +99 -0
- package/src/utils/validation.ts +1019 -0
- package/src/utils.ts +166 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { resolvePromptCacheKey } from "../auth-gateway/http";
|
|
3
|
+
/**
|
|
4
|
+
* Parsed inbound OpenAI chat-completions request, ready to feed into pi-ai
|
|
5
|
+
* `stream(model, context, options)`.
|
|
6
|
+
*/
|
|
7
|
+
import type { AuthGatewayParsedRequest as ParsedRequest } from "../auth-gateway/types";
|
|
8
|
+
import type {
|
|
9
|
+
AssistantMessage,
|
|
10
|
+
AssistantMessageEventStream,
|
|
11
|
+
Context,
|
|
12
|
+
ImageContent,
|
|
13
|
+
Message,
|
|
14
|
+
ResolvedServiceTier,
|
|
15
|
+
StopReason,
|
|
16
|
+
TextContent,
|
|
17
|
+
Tool,
|
|
18
|
+
ToolCall,
|
|
19
|
+
ToolResultMessage,
|
|
20
|
+
TSchema,
|
|
21
|
+
} from "../types";
|
|
22
|
+
import {
|
|
23
|
+
type OpenAIChatContentPart,
|
|
24
|
+
type OpenAIChatMessage,
|
|
25
|
+
type OpenAIChatTool,
|
|
26
|
+
type OpenAIChatToolCall,
|
|
27
|
+
type OpenAIChatToolChoice,
|
|
28
|
+
openaiChatRequestSchema,
|
|
29
|
+
} from "./openai-chat-server-schema";
|
|
30
|
+
|
|
31
|
+
export type { ParsedRequest };
|
|
32
|
+
|
|
33
|
+
type ReasoningEffort = NonNullable<ParsedRequest["options"]["reasoning"]>;
|
|
34
|
+
|
|
35
|
+
function isReasoningEffort(value: unknown): value is ReasoningEffort {
|
|
36
|
+
return value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isServiceTier(value: unknown): value is ResolvedServiceTier {
|
|
40
|
+
return value === "auto" || value === "default" || value === "flex" || value === "scale" || value === "priority";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// parseRequest
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
|
|
48
|
+
// Header capture is centralized in `auth-gateway/server.ts` (allow-listed
|
|
49
|
+
// headers like openai-organization/openai-project/openai-beta/x-stainless-*
|
|
50
|
+
// land on `options.headers` automatically). We consult `headers` here too
|
|
51
|
+
// for `resolvePromptCacheKey` to pull a cache identity out of inbound
|
|
52
|
+
// vendor-neutral headers when the body doesn't carry one.
|
|
53
|
+
const parsed = openaiChatRequestSchema.safeParse(body);
|
|
54
|
+
if (!parsed.success) {
|
|
55
|
+
throw new Error(`openai-chat: ${parsed.error.message}`);
|
|
56
|
+
}
|
|
57
|
+
const data = parsed.data;
|
|
58
|
+
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
const systemParts: string[] = [];
|
|
61
|
+
const messages: Message[] = [];
|
|
62
|
+
|
|
63
|
+
for (const m of data.messages as OpenAIChatMessage[]) {
|
|
64
|
+
switch (m.role) {
|
|
65
|
+
case "system": {
|
|
66
|
+
const text = stringifyContent(m.content);
|
|
67
|
+
if (text.length > 0) systemParts.push(text);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "developer":
|
|
71
|
+
messages.push({ role: "developer", content: parseUserLikeContent(m.content), timestamp: now });
|
|
72
|
+
break;
|
|
73
|
+
case "user":
|
|
74
|
+
messages.push({ role: "user", content: parseUserLikeContent(m.content), timestamp: now });
|
|
75
|
+
break;
|
|
76
|
+
case "assistant":
|
|
77
|
+
messages.push(
|
|
78
|
+
buildAssistantMessage(
|
|
79
|
+
(m.content ?? undefined) as string | OpenAIChatContentPart[] | undefined,
|
|
80
|
+
m.tool_calls,
|
|
81
|
+
data.model,
|
|
82
|
+
now,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
break;
|
|
86
|
+
case "tool":
|
|
87
|
+
pushToolResultMessages(messages, m.content, m.tool_call_id, undefined, now);
|
|
88
|
+
break;
|
|
89
|
+
case "function": {
|
|
90
|
+
// Legacy `function` role (pre-tools API): the message carries the tool's
|
|
91
|
+
// name on `name` and its output on `content`. Translate to a canonical
|
|
92
|
+
// `toolResult` with a synthetic id (no original id on the wire).
|
|
93
|
+
const fn = m as { role: "function"; name: string; content: string | null };
|
|
94
|
+
pushToolResultMessages(messages, fn.content ?? "", undefined, fn.name, now);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const tools = data.tools ? buildTools(data.tools as OpenAIChatTool[]) : undefined;
|
|
101
|
+
|
|
102
|
+
const context: Context = {
|
|
103
|
+
messages,
|
|
104
|
+
...(systemParts.length > 0 ? { systemPrompt: [systemParts.join("\n\n")] } : {}),
|
|
105
|
+
...(tools ? { tools } : {}),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Prefer max_completion_tokens (newer) over max_tokens.
|
|
109
|
+
const maxOutputTokens = data.max_completion_tokens ?? data.max_tokens;
|
|
110
|
+
const stopSequences = normalizeStop(data.stop);
|
|
111
|
+
// Schema accepts the Anthropic-style {type:'tool', name} variant that the SDK
|
|
112
|
+
// union doesn't model; the normalizer collapses it to a plain name lookup.
|
|
113
|
+
const toolChoice = normalizeToolChoice(data.tool_choice as Parameters<typeof normalizeToolChoice>[0]);
|
|
114
|
+
const includeStreamingUsage = data.stream_options?.include_usage === true;
|
|
115
|
+
|
|
116
|
+
// `includeStreamingUsage` is the one genuinely-opaque flag — the streaming
|
|
117
|
+
// encoder reads it later off `options.extra`. Everything else now lives on
|
|
118
|
+
// a typed field; `extra` stays undefined when only typed values are set.
|
|
119
|
+
const extra: Record<string, unknown> = {};
|
|
120
|
+
let hasExtra = false;
|
|
121
|
+
if (includeStreamingUsage) {
|
|
122
|
+
extra.includeStreamingUsage = true;
|
|
123
|
+
hasExtra = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const options: ParsedRequest["options"] = {};
|
|
127
|
+
if (maxOutputTokens !== undefined) options.maxOutputTokens = maxOutputTokens;
|
|
128
|
+
if (data.temperature !== undefined) options.temperature = data.temperature;
|
|
129
|
+
if (data.top_p !== undefined) options.topP = data.top_p;
|
|
130
|
+
if (stopSequences) options.stopSequences = stopSequences;
|
|
131
|
+
if (toolChoice !== undefined) options.toolChoice = toolChoice;
|
|
132
|
+
if (data.presence_penalty !== undefined) options.presencePenalty = data.presence_penalty;
|
|
133
|
+
if (data.frequency_penalty !== undefined) options.frequencyPenalty = data.frequency_penalty;
|
|
134
|
+
if (data.seed !== undefined) options.seed = data.seed;
|
|
135
|
+
if (data.logit_bias !== undefined) options.logitBias = data.logit_bias;
|
|
136
|
+
if (data.user !== undefined) options.user = data.user;
|
|
137
|
+
if (data.response_format !== undefined) options.responseFormat = data.response_format;
|
|
138
|
+
if (data.parallel_tool_calls !== undefined) options.parallelToolCalls = data.parallel_tool_calls;
|
|
139
|
+
if (data.reasoning_effort !== undefined && isReasoningEffort(data.reasoning_effort)) {
|
|
140
|
+
options.reasoning = data.reasoning_effort;
|
|
141
|
+
}
|
|
142
|
+
if (data.service_tier !== undefined && isServiceTier(data.service_tier)) {
|
|
143
|
+
options.serviceTier = data.service_tier;
|
|
144
|
+
}
|
|
145
|
+
if (data.metadata !== undefined) options.metadata = data.metadata;
|
|
146
|
+
const cacheKey = resolvePromptCacheKey(body, headers);
|
|
147
|
+
if (cacheKey !== undefined) options.promptCacheKey = cacheKey;
|
|
148
|
+
if (hasExtra) options.extra = extra;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
modelId: data.model,
|
|
152
|
+
context,
|
|
153
|
+
stream: data.stream === true,
|
|
154
|
+
options,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function stringifyContent(content: string | OpenAIChatContentPart[] | undefined): string {
|
|
159
|
+
if (content === undefined) return "";
|
|
160
|
+
if (typeof content === "string") return content;
|
|
161
|
+
const out: string[] = [];
|
|
162
|
+
for (const part of content) {
|
|
163
|
+
if (part.type === "text") out.push(part.text);
|
|
164
|
+
}
|
|
165
|
+
return out.join("");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function parseUserLikeContent(
|
|
169
|
+
content: string | OpenAIChatContentPart[] | undefined,
|
|
170
|
+
): string | (TextContent | ImageContent)[] {
|
|
171
|
+
if (content === undefined) return "";
|
|
172
|
+
if (typeof content === "string") return content;
|
|
173
|
+
const parts: (TextContent | ImageContent)[] = [];
|
|
174
|
+
for (const part of content) {
|
|
175
|
+
if (part.type === "text") {
|
|
176
|
+
parts.push({ type: "text", text: part.text });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (part.type !== "image_url") continue;
|
|
180
|
+
// input_audio / file / refusal / unknown-type parts are accepted by the
|
|
181
|
+
// schema for forward-compat but dropped here — pi-ai's canonical user
|
|
182
|
+
// content only models text and image today.
|
|
183
|
+
const url = typeof part.image_url === "string" ? part.image_url : part.image_url.url;
|
|
184
|
+
const decoded = decodeDataUri(url);
|
|
185
|
+
if (decoded) {
|
|
186
|
+
parts.push({ type: "image", data: decoded.data, mimeType: decoded.mimeType });
|
|
187
|
+
} else {
|
|
188
|
+
// No image fetcher available in the gateway; surface as a text placeholder so
|
|
189
|
+
// downstream providers still receive a coherent message.
|
|
190
|
+
parts.push({ type: "text", text: `[image: ${url}]` });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return parts;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function decodeDataUri(url: string): { data: string; mimeType: string } | undefined {
|
|
197
|
+
if (!url.startsWith("data:")) return undefined;
|
|
198
|
+
const comma = url.indexOf(",");
|
|
199
|
+
if (comma < 0) return undefined;
|
|
200
|
+
const header = url.slice(5, comma);
|
|
201
|
+
const payload = url.slice(comma + 1);
|
|
202
|
+
const isBase64 = header.endsWith(";base64");
|
|
203
|
+
const mimeType = (isBase64 ? header.slice(0, -";base64".length) : header) || "application/octet-stream";
|
|
204
|
+
const data = isBase64 ? payload : Buffer.from(decodeURIComponent(payload), "utf8").toString("base64");
|
|
205
|
+
return { data, mimeType };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildAssistantMessage(
|
|
209
|
+
content: string | OpenAIChatContentPart[] | undefined,
|
|
210
|
+
toolCalls: OpenAIChatToolCall[] | undefined,
|
|
211
|
+
modelId: string,
|
|
212
|
+
now: number,
|
|
213
|
+
): AssistantMessage {
|
|
214
|
+
const parts: AssistantMessage["content"] = [];
|
|
215
|
+
const text = stringifyContent(content);
|
|
216
|
+
if (text.length > 0) parts.push({ type: "text", text });
|
|
217
|
+
if (toolCalls) {
|
|
218
|
+
for (const raw of toolCalls) {
|
|
219
|
+
// Schema only accepts type:"function" (or omitted); narrow the SDK
|
|
220
|
+
// union here so the custom-tool variant doesn't trip TS.
|
|
221
|
+
if (raw.type !== undefined && raw.type !== "function") continue;
|
|
222
|
+
const fn = (raw as { function: { name: string; arguments: string } }).function;
|
|
223
|
+
const argsStr = fn.arguments;
|
|
224
|
+
let args: Record<string, unknown> = {};
|
|
225
|
+
if (argsStr.length > 0) {
|
|
226
|
+
try {
|
|
227
|
+
const v: unknown = JSON.parse(argsStr);
|
|
228
|
+
args =
|
|
229
|
+
v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>) : { __raw: argsStr };
|
|
230
|
+
} catch {
|
|
231
|
+
args = { __raw: argsStr };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const call: ToolCall = { type: "toolCall", id: raw.id, name: fn.name, arguments: args };
|
|
235
|
+
parts.push(call);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
role: "assistant",
|
|
240
|
+
content: parts,
|
|
241
|
+
api: "openai-completions",
|
|
242
|
+
provider: "openai",
|
|
243
|
+
model: modelId,
|
|
244
|
+
usage: {
|
|
245
|
+
input: 0,
|
|
246
|
+
output: 0,
|
|
247
|
+
cacheRead: 0,
|
|
248
|
+
cacheWrite: 0,
|
|
249
|
+
totalTokens: 0,
|
|
250
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
251
|
+
},
|
|
252
|
+
stopReason: "stop",
|
|
253
|
+
timestamp: now,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Walk a wire `tool` (or legacy `function`) message into canonical messages.
|
|
259
|
+
* Tool-result content may carry images alongside text; pi-ai's
|
|
260
|
+
* `ToolResultMessage` accepts both, but most downstream providers ignore
|
|
261
|
+
* images on tool results. To mirror Rust's `encode_messages` behavior we
|
|
262
|
+
* keep text inside the tool-result message and hoist any image parts into a
|
|
263
|
+
* follow-up `user` message so they still reach the model.
|
|
264
|
+
*/
|
|
265
|
+
function pushToolResultMessages(
|
|
266
|
+
messages: Message[],
|
|
267
|
+
content: string | OpenAIChatContentPart[] | undefined | null,
|
|
268
|
+
toolCallId: string | undefined,
|
|
269
|
+
toolName: string | undefined,
|
|
270
|
+
now: number,
|
|
271
|
+
): void {
|
|
272
|
+
const textParts: TextContent[] = [];
|
|
273
|
+
const imageParts: ImageContent[] = [];
|
|
274
|
+
|
|
275
|
+
if (typeof content === "string") {
|
|
276
|
+
if (content.length > 0) textParts.push({ type: "text", text: content });
|
|
277
|
+
} else if (Array.isArray(content)) {
|
|
278
|
+
for (const part of content) {
|
|
279
|
+
if (part.type === "text") {
|
|
280
|
+
textParts.push({ type: "text", text: part.text });
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (part.type !== "image_url") continue;
|
|
284
|
+
const url = typeof part.image_url === "string" ? part.image_url : part.image_url.url;
|
|
285
|
+
const decoded = decodeDataUri(url);
|
|
286
|
+
if (decoded) {
|
|
287
|
+
imageParts.push({ type: "image", data: decoded.data, mimeType: decoded.mimeType });
|
|
288
|
+
} else {
|
|
289
|
+
// No fetcher available; degrade gracefully to a text placeholder.
|
|
290
|
+
textParts.push({ type: "text", text: `[image: ${url}]` });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const toolMsg: ToolResultMessage = {
|
|
296
|
+
role: "toolResult",
|
|
297
|
+
toolCallId: toolCallId ?? "",
|
|
298
|
+
// OpenAI's `tool` role omits the tool name on the wire; the legacy
|
|
299
|
+
// `function` role supplies it. Downstream providers tolerate empty.
|
|
300
|
+
toolName: toolName ?? "",
|
|
301
|
+
content: textParts.length > 0 ? textParts : [{ type: "text", text: "" }],
|
|
302
|
+
isError: false,
|
|
303
|
+
timestamp: now,
|
|
304
|
+
};
|
|
305
|
+
messages.push(toolMsg);
|
|
306
|
+
|
|
307
|
+
if (imageParts.length > 0) {
|
|
308
|
+
messages.push({
|
|
309
|
+
role: "user",
|
|
310
|
+
content: imageParts,
|
|
311
|
+
timestamp: now,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function buildTools(tools: OpenAIChatTool[]): Tool[] | undefined {
|
|
317
|
+
if (tools.length === 0) return undefined;
|
|
318
|
+
const out: Tool[] = [];
|
|
319
|
+
for (const t of tools) {
|
|
320
|
+
if (t.type !== "function") continue;
|
|
321
|
+
out.push({
|
|
322
|
+
name: t.function.name,
|
|
323
|
+
description: t.function.description ?? "",
|
|
324
|
+
parameters: (t.function.parameters ?? {}) as Record<string, unknown> as TSchema,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function normalizeStop(value: string | string[] | undefined): string[] | undefined {
|
|
331
|
+
if (value === undefined) return undefined;
|
|
332
|
+
if (typeof value === "string") return [value];
|
|
333
|
+
return value.length > 0 ? value : undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function normalizeToolChoice(value: OpenAIChatToolChoice | undefined): ParsedRequest["options"]["toolChoice"] {
|
|
337
|
+
if (value === undefined) return undefined;
|
|
338
|
+
if (value === "auto" || value === "none" || value === "required") return value;
|
|
339
|
+
if (typeof value === "object" && value !== null) {
|
|
340
|
+
// OpenAI canonical: { type: 'function', function: { name } }
|
|
341
|
+
if ("function" in value && value.function) return { name: value.function.name };
|
|
342
|
+
// Anthropic-style passthrough (schema-allowed): { type: 'tool', name }
|
|
343
|
+
const anthropicLike = value as unknown as { type?: string; name?: string };
|
|
344
|
+
if (anthropicLike.type === "tool" && typeof anthropicLike.name === "string") {
|
|
345
|
+
return { name: anthropicLike.name };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// encodeResponse (non-streaming)
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
export function encodeResponse(message: AssistantMessage, requestedModelId: string): Record<string, unknown> {
|
|
356
|
+
const { text, reasoning, toolCalls } = flattenAssistant(message);
|
|
357
|
+
|
|
358
|
+
const responseMessage: Record<string, unknown> = {
|
|
359
|
+
role: "assistant",
|
|
360
|
+
content: text.length > 0 ? text : null,
|
|
361
|
+
// pi-ai does not surface real refusals yet; emit `null` so SDKs that
|
|
362
|
+
// probe `.refusal` see the documented field shape rather than missing.
|
|
363
|
+
refusal: null,
|
|
364
|
+
};
|
|
365
|
+
if (reasoning.length > 0) {
|
|
366
|
+
// DeepSeek-style / o-series reasoning channel.
|
|
367
|
+
responseMessage.reasoning_content = reasoning;
|
|
368
|
+
}
|
|
369
|
+
if (toolCalls.length > 0) {
|
|
370
|
+
responseMessage.tool_calls = toolCalls.map(tc => ({
|
|
371
|
+
id: tc.id,
|
|
372
|
+
type: "function",
|
|
373
|
+
function: { name: tc.name, arguments: stringifyArgs(tc.arguments) },
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
id: makeId(),
|
|
379
|
+
object: "chat.completion",
|
|
380
|
+
created: Math.floor(Date.now() / 1000),
|
|
381
|
+
model: requestedModelId,
|
|
382
|
+
// Real OpenAI always emits this key, even when the value is null. Mirror
|
|
383
|
+
// the contract so probing SDKs do not throw on a missing field.
|
|
384
|
+
system_fingerprint: null,
|
|
385
|
+
choices: [
|
|
386
|
+
{
|
|
387
|
+
index: 0,
|
|
388
|
+
message: responseMessage,
|
|
389
|
+
finish_reason: mapFinishReason(message.stopReason, toolCalls.length > 0),
|
|
390
|
+
logprobs: null,
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
usage: buildUsage(message),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function buildUsage(message: AssistantMessage): Record<string, unknown> {
|
|
398
|
+
const promptTokens = message.usage.input + message.usage.cacheRead + message.usage.cacheWrite;
|
|
399
|
+
const usage: Record<string, unknown> = {
|
|
400
|
+
prompt_tokens: promptTokens,
|
|
401
|
+
completion_tokens: message.usage.output,
|
|
402
|
+
total_tokens: promptTokens + message.usage.output,
|
|
403
|
+
prompt_tokens_details: { cached_tokens: message.usage.cacheRead },
|
|
404
|
+
};
|
|
405
|
+
if (message.usage.reasoningTokens !== undefined) {
|
|
406
|
+
usage.completion_tokens_details = { reasoning_tokens: message.usage.reasoningTokens };
|
|
407
|
+
}
|
|
408
|
+
return usage;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function flattenAssistant(message: AssistantMessage): {
|
|
412
|
+
text: string;
|
|
413
|
+
reasoning: string;
|
|
414
|
+
toolCalls: ToolCall[];
|
|
415
|
+
} {
|
|
416
|
+
let text = "";
|
|
417
|
+
let reasoning = "";
|
|
418
|
+
const toolCalls: ToolCall[] = [];
|
|
419
|
+
for (const part of message.content) {
|
|
420
|
+
switch (part.type) {
|
|
421
|
+
case "text":
|
|
422
|
+
text += part.text;
|
|
423
|
+
break;
|
|
424
|
+
case "thinking":
|
|
425
|
+
reasoning += part.thinking;
|
|
426
|
+
break;
|
|
427
|
+
case "redactedThinking":
|
|
428
|
+
// Opaque blob — surface verbatim on the reasoning channel so the
|
|
429
|
+
// concatenation round-trips through clients that just echo it.
|
|
430
|
+
reasoning += part.data;
|
|
431
|
+
break;
|
|
432
|
+
case "toolCall":
|
|
433
|
+
toolCalls.push(part);
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return { text, reasoning, toolCalls };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function isOnlyRaw(args: Record<string, unknown>): boolean {
|
|
441
|
+
for (const k in args) {
|
|
442
|
+
if (k !== "__raw") return false;
|
|
443
|
+
}
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function stringifyArgs(args: Record<string, unknown>): string {
|
|
448
|
+
// `__raw` is our fallback marker for un-parseable inbound args; preserve it verbatim on the way out.
|
|
449
|
+
if (typeof args.__raw === "string" && isOnlyRaw(args)) return args.__raw;
|
|
450
|
+
try {
|
|
451
|
+
return JSON.stringify(args);
|
|
452
|
+
} catch {
|
|
453
|
+
return "{}";
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function mapFinishReason(reason: StopReason, hasToolCalls: boolean): string {
|
|
458
|
+
if (reason === "toolUse" || (hasToolCalls && reason === "stop")) return "tool_calls";
|
|
459
|
+
if (reason === "length") return "length";
|
|
460
|
+
// pi-ai's StopReason does not currently carry a content-filter signal;
|
|
461
|
+
// when it does, map it to "content_filter" here.
|
|
462
|
+
return "stop";
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function makeId(): string {
|
|
466
|
+
return `chatcmpl-${randomUUID()}`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
// encodeStream (SSE)
|
|
471
|
+
// ---------------------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
export function encodeStream(
|
|
474
|
+
events: AssistantMessageEventStream,
|
|
475
|
+
requestedModelId: string,
|
|
476
|
+
options?: ParsedRequest["options"],
|
|
477
|
+
): ReadableStream<Uint8Array> {
|
|
478
|
+
const encoder = new TextEncoder();
|
|
479
|
+
const id = makeId();
|
|
480
|
+
const created = Math.floor(Date.now() / 1000);
|
|
481
|
+
const includeUsage = options?.extra?.includeStreamingUsage === true;
|
|
482
|
+
|
|
483
|
+
const baseChunk = (delta: Record<string, unknown>, finishReason: string | null) => ({
|
|
484
|
+
id,
|
|
485
|
+
object: "chat.completion.chunk",
|
|
486
|
+
created,
|
|
487
|
+
model: requestedModelId,
|
|
488
|
+
system_fingerprint: null,
|
|
489
|
+
choices: [{ index: 0, delta, finish_reason: finishReason, logprobs: null }],
|
|
490
|
+
...(includeUsage ? { usage: null } : {}),
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const writeSse = (controller: ReadableStreamDefaultController<Uint8Array>, payload: unknown): void => {
|
|
494
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(payload)}\n\n`));
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const writeUsage = (controller: ReadableStreamDefaultController<Uint8Array>, message: AssistantMessage): void => {
|
|
498
|
+
writeSse(controller, {
|
|
499
|
+
id,
|
|
500
|
+
object: "chat.completion.chunk",
|
|
501
|
+
created,
|
|
502
|
+
model: requestedModelId,
|
|
503
|
+
system_fingerprint: null,
|
|
504
|
+
choices: [],
|
|
505
|
+
usage: buildUsage(message),
|
|
506
|
+
});
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
return new ReadableStream<Uint8Array>({
|
|
510
|
+
async start(controller) {
|
|
511
|
+
// contentIndex (from pi-ai events) -> tool_calls index on the wire.
|
|
512
|
+
const toolIndexByContentIndex = new Map<number, number>();
|
|
513
|
+
let nextToolIndex = 0;
|
|
514
|
+
let hasToolCalls = false;
|
|
515
|
+
let finishReason: string = "stop";
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
// Initial role chunk.
|
|
519
|
+
writeSse(controller, baseChunk({ role: "assistant" }, null));
|
|
520
|
+
|
|
521
|
+
for await (const event of events) {
|
|
522
|
+
switch (event.type) {
|
|
523
|
+
case "text_delta":
|
|
524
|
+
if (event.delta.length > 0) {
|
|
525
|
+
writeSse(controller, baseChunk({ content: event.delta }, null));
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
|
|
529
|
+
case "thinking_delta":
|
|
530
|
+
// DeepSeek-style / o-series reasoning channel. Clients that don't
|
|
531
|
+
// understand it ignore the unknown delta key.
|
|
532
|
+
if (event.delta.length > 0) {
|
|
533
|
+
writeSse(controller, baseChunk({ reasoning_content: event.delta }, null));
|
|
534
|
+
}
|
|
535
|
+
break;
|
|
536
|
+
|
|
537
|
+
case "toolcall_start": {
|
|
538
|
+
hasToolCalls = true;
|
|
539
|
+
const idx = nextToolIndex++;
|
|
540
|
+
toolIndexByContentIndex.set(event.contentIndex, idx);
|
|
541
|
+
const partial = event.partial.content[event.contentIndex];
|
|
542
|
+
const call = partial && partial.type === "toolCall" ? partial : undefined;
|
|
543
|
+
writeSse(
|
|
544
|
+
controller,
|
|
545
|
+
baseChunk(
|
|
546
|
+
{
|
|
547
|
+
tool_calls: [
|
|
548
|
+
{
|
|
549
|
+
index: idx,
|
|
550
|
+
id: call?.id ?? "",
|
|
551
|
+
type: "function",
|
|
552
|
+
function: { name: call?.name ?? "", arguments: "" },
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
},
|
|
556
|
+
null,
|
|
557
|
+
),
|
|
558
|
+
);
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
case "toolcall_delta": {
|
|
563
|
+
const idx = toolIndexByContentIndex.get(event.contentIndex);
|
|
564
|
+
if (idx === undefined) break;
|
|
565
|
+
writeSse(
|
|
566
|
+
controller,
|
|
567
|
+
baseChunk({ tool_calls: [{ index: idx, function: { arguments: event.delta } }] }, null),
|
|
568
|
+
);
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
case "done":
|
|
573
|
+
finishReason =
|
|
574
|
+
event.reason === "toolUse"
|
|
575
|
+
? "tool_calls"
|
|
576
|
+
: event.reason === "length"
|
|
577
|
+
? "length"
|
|
578
|
+
: hasToolCalls
|
|
579
|
+
? "tool_calls"
|
|
580
|
+
: "stop";
|
|
581
|
+
writeSse(controller, baseChunk({}, finishReason));
|
|
582
|
+
if (includeUsage) writeUsage(controller, event.message);
|
|
583
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
584
|
+
controller.close();
|
|
585
|
+
return;
|
|
586
|
+
|
|
587
|
+
case "error": {
|
|
588
|
+
const msg = event.error.errorMessage ?? "stream error";
|
|
589
|
+
writeSse(controller, { error: { message: msg, type: "upstream_error" } });
|
|
590
|
+
controller.close();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Drop start / *_start / *_end — chat-completions wire only
|
|
595
|
+
// surfaces deltas and the terminal finish_reason.
|
|
596
|
+
default:
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Stream ended without a terminal `done` (defensive). Close gracefully.
|
|
602
|
+
writeSse(controller, baseChunk({}, hasToolCalls ? "tool_calls" : "stop"));
|
|
603
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
604
|
+
controller.close();
|
|
605
|
+
} catch (err) {
|
|
606
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
607
|
+
writeSse(controller, { error: { message: msg, type: "upstream_error" } });
|
|
608
|
+
controller.close();
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ---------------------------------------------------------------------------
|
|
615
|
+
// formatError
|
|
616
|
+
// ---------------------------------------------------------------------------
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* OpenAI chat-completions error envelope:
|
|
620
|
+
* `{ error: { message, type } }`
|
|
621
|
+
* Matches the shape the official SDK auto-parses into `APIError`.
|
|
622
|
+
*/
|
|
623
|
+
export function formatError(status: number, type: string, message: string): Response {
|
|
624
|
+
return new Response(JSON.stringify({ error: { message, type } }), {
|
|
625
|
+
status,
|
|
626
|
+
headers: { "Content-Type": "application/json" },
|
|
627
|
+
});
|
|
628
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for OpenAI code provider (ChatGPT OAuth) backend
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
6
|
+
|
|
7
|
+
export const OPENAI_HEADERS = {
|
|
8
|
+
BETA: "OpenAI-Beta",
|
|
9
|
+
ACCOUNT_ID: "chatgpt-account-id",
|
|
10
|
+
ORIGINATOR: "originator",
|
|
11
|
+
SESSION_ID: "session_id",
|
|
12
|
+
CONVERSATION_ID: "conversation_id",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export const OPENAI_HEADER_VALUES = {
|
|
16
|
+
BETA_RESPONSES: "responses=experimental",
|
|
17
|
+
BETA_RESPONSES_WEBSOCKETS_V2: "responses_websockets=2026-02-06",
|
|
18
|
+
ORIGINATOR_CODEX: "pi",
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export const URL_PATHS = {
|
|
22
|
+
RESPONSES: "/responses",
|
|
23
|
+
CODEX_RESPONSES: "/codex/responses",
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
export const JWT_CLAIM_PATH = "https://api.openai.com/auth" as const;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extract account ID from a OpenAI code backend JWT access token.
|
|
30
|
+
* Returns undefined if the token is not a valid OpenAI code backend JWT.
|
|
31
|
+
*/
|
|
32
|
+
export function getCodexAccountId(accessToken: string): string | undefined {
|
|
33
|
+
try {
|
|
34
|
+
const parts = accessToken.split(".");
|
|
35
|
+
if (parts.length !== 3) return undefined;
|
|
36
|
+
const decoded = Buffer.from(parts[1] ?? "", "base64").toString("utf-8");
|
|
37
|
+
const payload = JSON.parse(decoded) as Record<string, unknown>;
|
|
38
|
+
const auth = payload[JWT_CLAIM_PATH] as { chatgpt_account_id?: string } | undefined;
|
|
39
|
+
return auth?.chatgpt_account_id ?? undefined;
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|