@hebo-ai/gateway 0.3.0-rc.1 → 0.3.0-rc.2
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/dist/config.d.ts +2 -0
- package/dist/config.js +61 -0
- package/dist/endpoints/chat-completions/converters.d.ts +36 -0
- package/dist/endpoints/chat-completions/converters.js +373 -0
- package/dist/endpoints/chat-completions/handler.d.ts +2 -0
- package/dist/endpoints/chat-completions/handler.js +99 -0
- package/dist/endpoints/chat-completions/index.d.ts +3 -0
- package/dist/endpoints/chat-completions/index.js +3 -0
- package/dist/endpoints/chat-completions/schema.d.ts +476 -0
- package/dist/endpoints/chat-completions/schema.js +192 -0
- package/dist/endpoints/embeddings/converters.d.ts +10 -0
- package/dist/endpoints/embeddings/converters.js +31 -0
- package/dist/endpoints/embeddings/handler.d.ts +2 -0
- package/dist/endpoints/embeddings/handler.js +69 -0
- package/dist/endpoints/embeddings/index.d.ts +3 -0
- package/dist/endpoints/embeddings/index.js +3 -0
- package/dist/endpoints/embeddings/schema.d.ts +38 -0
- package/dist/endpoints/embeddings/schema.js +26 -0
- package/dist/endpoints/models/converters.d.ts +6 -0
- package/dist/endpoints/models/converters.js +42 -0
- package/dist/endpoints/models/handler.d.ts +2 -0
- package/dist/endpoints/models/handler.js +29 -0
- package/dist/endpoints/models/index.d.ts +3 -0
- package/dist/endpoints/models/index.js +3 -0
- package/dist/endpoints/models/schema.d.ts +42 -0
- package/dist/endpoints/models/schema.js +31 -0
- package/dist/errors/ai-sdk.d.ts +2 -0
- package/dist/errors/ai-sdk.js +52 -0
- package/dist/errors/gateway.d.ts +5 -0
- package/dist/errors/gateway.js +13 -0
- package/dist/errors/openai.d.ts +20 -0
- package/dist/errors/openai.js +29 -0
- package/dist/errors/utils.d.ts +24 -0
- package/dist/errors/utils.js +47 -0
- package/dist/gateway.d.ts +9 -0
- package/dist/gateway.js +36 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -0
- package/dist/lifecycle.d.ts +2 -0
- package/dist/lifecycle.js +46 -0
- package/dist/logger/default.d.ts +5 -0
- package/dist/logger/default.js +62 -0
- package/dist/logger/index.d.ts +15 -0
- package/dist/logger/index.js +25 -0
- package/dist/middleware/common.d.ts +12 -0
- package/dist/middleware/common.js +145 -0
- package/dist/middleware/matcher.d.ts +27 -0
- package/dist/middleware/matcher.js +110 -0
- package/dist/middleware/utils.d.ts +2 -0
- package/dist/middleware/utils.js +26 -0
- package/dist/models/amazon/index.d.ts +2 -0
- package/dist/models/amazon/index.js +2 -0
- package/dist/models/amazon/middleware.d.ts +3 -0
- package/dist/models/amazon/middleware.js +64 -0
- package/dist/models/amazon/presets.d.ts +2390 -0
- package/dist/models/amazon/presets.js +80 -0
- package/dist/models/anthropic/index.d.ts +2 -0
- package/dist/models/anthropic/index.js +2 -0
- package/dist/models/anthropic/middleware.d.ts +2 -0
- package/dist/models/anthropic/middleware.js +49 -0
- package/dist/models/anthropic/presets.d.ts +4106 -0
- package/dist/models/anthropic/presets.js +113 -0
- package/dist/models/catalog.d.ts +4 -0
- package/dist/models/catalog.js +4 -0
- package/dist/models/cohere/index.d.ts +2 -0
- package/dist/models/cohere/index.js +2 -0
- package/dist/models/cohere/middleware.d.ts +3 -0
- package/dist/models/cohere/middleware.js +60 -0
- package/dist/models/cohere/presets.d.ts +2918 -0
- package/dist/models/cohere/presets.js +134 -0
- package/dist/models/google/index.d.ts +2 -0
- package/dist/models/google/index.js +2 -0
- package/dist/models/google/middleware.d.ts +7 -0
- package/dist/models/google/middleware.js +86 -0
- package/dist/models/google/presets.d.ts +2166 -0
- package/dist/models/google/presets.js +76 -0
- package/dist/models/meta/index.d.ts +1 -0
- package/dist/models/meta/index.js +1 -0
- package/dist/models/meta/presets.d.ts +3254 -0
- package/dist/models/meta/presets.js +95 -0
- package/dist/models/openai/index.d.ts +2 -0
- package/dist/models/openai/index.js +2 -0
- package/dist/models/openai/middleware.d.ts +3 -0
- package/dist/models/openai/middleware.js +61 -0
- package/dist/models/openai/presets.d.ts +6252 -0
- package/dist/models/openai/presets.js +206 -0
- package/dist/models/types.d.ts +20 -0
- package/dist/models/types.js +80 -0
- package/dist/models/voyage/index.d.ts +2 -0
- package/dist/models/voyage/index.js +2 -0
- package/dist/models/voyage/middleware.d.ts +2 -0
- package/dist/models/voyage/middleware.js +18 -0
- package/dist/models/voyage/presets.d.ts +3471 -0
- package/dist/models/voyage/presets.js +85 -0
- package/dist/providers/anthropic/canonical.d.ts +3 -0
- package/dist/providers/anthropic/canonical.js +9 -0
- package/dist/providers/anthropic/index.d.ts +1 -0
- package/dist/providers/anthropic/index.js +1 -0
- package/dist/providers/bedrock/canonical.d.ts +17 -0
- package/dist/providers/bedrock/canonical.js +59 -0
- package/dist/providers/bedrock/index.d.ts +1 -0
- package/dist/providers/bedrock/index.js +1 -0
- package/dist/providers/cohere/canonical.d.ts +3 -0
- package/dist/providers/cohere/canonical.js +17 -0
- package/dist/providers/cohere/index.d.ts +1 -0
- package/dist/providers/cohere/index.js +1 -0
- package/dist/providers/groq/canonical.d.ts +3 -0
- package/dist/providers/groq/canonical.js +12 -0
- package/dist/providers/groq/index.d.ts +1 -0
- package/dist/providers/groq/index.js +1 -0
- package/dist/providers/openai/canonical.d.ts +3 -0
- package/dist/providers/openai/canonical.js +8 -0
- package/dist/providers/openai/index.d.ts +1 -0
- package/dist/providers/openai/index.js +1 -0
- package/dist/providers/registry.d.ts +24 -0
- package/dist/providers/registry.js +99 -0
- package/dist/providers/types.d.ts +7 -0
- package/dist/providers/types.js +11 -0
- package/dist/providers/vertex/canonical.d.ts +3 -0
- package/dist/providers/vertex/canonical.js +8 -0
- package/dist/providers/vertex/index.d.ts +1 -0
- package/dist/providers/vertex/index.js +1 -0
- package/dist/providers/voyage/canonical.d.ts +3 -0
- package/dist/providers/voyage/canonical.js +7 -0
- package/dist/providers/voyage/index.d.ts +1 -0
- package/dist/providers/voyage/index.js +1 -0
- package/dist/telemetry/access-log.d.ts +2 -0
- package/dist/telemetry/access-log.js +46 -0
- package/dist/telemetry/stream.d.ts +9 -0
- package/dist/telemetry/stream.js +59 -0
- package/dist/telemetry/utils.d.ts +4 -0
- package/dist/telemetry/utils.js +41 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.js +1 -0
- package/dist/utils/env.d.ts +2 -0
- package/dist/utils/env.js +5 -0
- package/dist/utils/preset.d.ts +9 -0
- package/dist/utils/preset.js +41 -0
- package/dist/utils/request.d.ts +8 -0
- package/dist/utils/request.js +48 -0
- package/dist/utils/response.d.ts +2 -0
- package/dist/utils/response.js +42 -0
- package/package.json +1 -1
- package/src/endpoints/chat-completions/handler.ts +1 -7
- package/src/endpoints/embeddings/handler.ts +1 -9
- package/src/middleware/matcher.ts +77 -53
- package/src/models/amazon/middleware.test.ts +4 -4
- package/src/models/amazon/middleware.ts +2 -2
- package/src/models/anthropic/middleware.test.ts +5 -5
- package/src/models/anthropic/middleware.ts +1 -1
- package/src/models/cohere/middleware.test.ts +4 -4
- package/src/models/cohere/middleware.ts +2 -2
- package/src/models/google/middleware.test.ts +4 -4
- package/src/models/google/middleware.ts +2 -2
- package/src/models/openai/middleware.test.ts +5 -5
- package/src/models/openai/middleware.ts +2 -2
- package/src/models/voyage/middleware.test.ts +2 -2
- package/src/models/voyage/middleware.ts +1 -1
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { isLogger, logger, setLoggerInstance } from "./logger";
|
|
2
|
+
import { createDefaultLogger } from "./logger/default";
|
|
3
|
+
import { kParsed } from "./types";
|
|
4
|
+
export const parseConfig = (config) => {
|
|
5
|
+
// If it has been parsed before, just return
|
|
6
|
+
if (kParsed in config)
|
|
7
|
+
return config;
|
|
8
|
+
const providers = config.providers ?? {};
|
|
9
|
+
const parsedProviders = {};
|
|
10
|
+
const models = config.models ?? {};
|
|
11
|
+
// Set the global logger instance
|
|
12
|
+
if (config.logger === undefined) {
|
|
13
|
+
setLoggerInstance(createDefaultLogger({}));
|
|
14
|
+
}
|
|
15
|
+
else if (config.logger !== null) {
|
|
16
|
+
setLoggerInstance(isLogger(config.logger) ? config.logger : createDefaultLogger(config.logger));
|
|
17
|
+
logger.info(isLogger(config.logger)
|
|
18
|
+
? `[logger] custom logger configured`
|
|
19
|
+
: `[logger] logger configured: level=${config.logger.level}`);
|
|
20
|
+
}
|
|
21
|
+
// Strip providers that are not configured
|
|
22
|
+
for (const id in providers) {
|
|
23
|
+
const provider = providers[id];
|
|
24
|
+
if (provider === undefined) {
|
|
25
|
+
logger.warn(`[config] ${id} provider removed (undefined)`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
parsedProviders[id] = provider;
|
|
29
|
+
}
|
|
30
|
+
if (Object.keys(parsedProviders).length === 0) {
|
|
31
|
+
throw new Error("No providers configured (config.providers is empty)");
|
|
32
|
+
}
|
|
33
|
+
// Strip providers that are not configured from models
|
|
34
|
+
const parsedModels = {};
|
|
35
|
+
const warnings = new Set();
|
|
36
|
+
for (const id in models) {
|
|
37
|
+
const model = models[id];
|
|
38
|
+
const kept = [];
|
|
39
|
+
for (const p of model.providers) {
|
|
40
|
+
if (p in parsedProviders)
|
|
41
|
+
kept.push(p);
|
|
42
|
+
else
|
|
43
|
+
warnings.add(p);
|
|
44
|
+
}
|
|
45
|
+
if (kept.length > 0)
|
|
46
|
+
parsedModels[id] = { ...model, providers: kept };
|
|
47
|
+
}
|
|
48
|
+
for (const warning of warnings) {
|
|
49
|
+
logger.warn(`[config] ${warning} provider removed (not configured)`);
|
|
50
|
+
}
|
|
51
|
+
if (Object.keys(parsedModels).length === 0) {
|
|
52
|
+
throw new Error("No models configured (config.models is empty)");
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
...config,
|
|
56
|
+
logger: config.logger,
|
|
57
|
+
providers: parsedProviders,
|
|
58
|
+
models: parsedModels,
|
|
59
|
+
[kParsed]: true,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SharedV3ProviderOptions, SharedV3ProviderMetadata } from "@ai-sdk/provider";
|
|
2
|
+
import type { GenerateTextResult, StreamTextResult, FinishReason, ToolChoice, ToolSet, ModelMessage, UserContent, LanguageModelUsage, Output, TextStreamPart, AssistantModelMessage, ToolModelMessage, UserModelMessage } from "ai";
|
|
3
|
+
import type { ChatCompletionsToolCall, ChatCompletionsTool, ChatCompletionsToolChoice, ChatCompletionsContentPart, ChatCompletionsMessage, ChatCompletionsUserMessage, ChatCompletionsAssistantMessage, ChatCompletionsToolMessage, ChatCompletionsFinishReason, ChatCompletionsUsage, ChatCompletionsInputs, ChatCompletions, ChatCompletionsChunk } from "./schema";
|
|
4
|
+
import { OpenAIError } from "../../errors/openai";
|
|
5
|
+
export type TextCallOptions = {
|
|
6
|
+
messages: ModelMessage[];
|
|
7
|
+
tools?: ToolSet;
|
|
8
|
+
toolChoice?: ToolChoice<ToolSet>;
|
|
9
|
+
temperature?: number;
|
|
10
|
+
maxOutputTokens?: number;
|
|
11
|
+
frequencyPenalty?: number;
|
|
12
|
+
presencePenalty?: number;
|
|
13
|
+
seed?: number;
|
|
14
|
+
stopSequences?: string[];
|
|
15
|
+
topP?: number;
|
|
16
|
+
providerOptions: SharedV3ProviderOptions;
|
|
17
|
+
};
|
|
18
|
+
export declare function convertToTextCallOptions(params: ChatCompletionsInputs): TextCallOptions;
|
|
19
|
+
export declare function convertToModelMessages(messages: ChatCompletionsMessage[]): ModelMessage[];
|
|
20
|
+
export declare function fromChatCompletionsUserMessage(message: ChatCompletionsUserMessage): UserModelMessage;
|
|
21
|
+
export declare function fromChatCompletionsAssistantMessage(message: ChatCompletionsAssistantMessage): AssistantModelMessage;
|
|
22
|
+
export declare function fromChatCompletionsToolResultMessage(message: ChatCompletionsAssistantMessage, toolById: Map<string, ChatCompletionsToolMessage>): ToolModelMessage | undefined;
|
|
23
|
+
export declare function fromChatCompletionsContent(content: ChatCompletionsContentPart[]): UserContent;
|
|
24
|
+
export declare const convertToToolSet: (tools: ChatCompletionsTool[] | undefined) => ToolSet | undefined;
|
|
25
|
+
export declare const convertToToolChoice: (toolChoice: ChatCompletionsToolChoice | undefined) => ToolChoice<ToolSet> | undefined;
|
|
26
|
+
export declare function toChatCompletions(result: GenerateTextResult<ToolSet, Output.Output>, model: string): ChatCompletions;
|
|
27
|
+
export declare function toChatCompletionsResponse(result: GenerateTextResult<ToolSet, Output.Output>, model: string, responseInit?: ResponseInit): Response;
|
|
28
|
+
export declare function toChatCompletionsStream(result: StreamTextResult<ToolSet, Output.Output>, model: string): ReadableStream<Uint8Array>;
|
|
29
|
+
export declare function toChatCompletionsStreamResponse(result: StreamTextResult<ToolSet, Output.Output>, model: string, responseInit?: ResponseInit): Response;
|
|
30
|
+
export declare class ChatCompletionsStream extends TransformStream<TextStreamPart<ToolSet>, ChatCompletionsChunk | OpenAIError> {
|
|
31
|
+
constructor(model: string);
|
|
32
|
+
}
|
|
33
|
+
export declare const toChatCompletionsAssistantMessage: (result: GenerateTextResult<ToolSet, Output.Output>) => ChatCompletionsAssistantMessage;
|
|
34
|
+
export declare function toChatCompletionsUsage(usage: LanguageModelUsage): ChatCompletionsUsage;
|
|
35
|
+
export declare function toChatCompletionsToolCall(id: string, name: string, args: unknown, providerMetadata?: SharedV3ProviderMetadata): ChatCompletionsToolCall;
|
|
36
|
+
export declare const toChatCompletionsFinishReason: (finishReason: FinishReason) => ChatCompletionsFinishReason;
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { convertBase64ToUint8Array } from "@ai-sdk/provider-utils";
|
|
2
|
+
import { jsonSchema, JsonToSseTransformStream, tool } from "ai";
|
|
3
|
+
import { GatewayError } from "../../errors/gateway";
|
|
4
|
+
import { OpenAIError, toOpenAIError } from "../../errors/openai";
|
|
5
|
+
import { toResponse } from "../../utils/response";
|
|
6
|
+
// --- Request Flow ---
|
|
7
|
+
export function convertToTextCallOptions(params) {
|
|
8
|
+
const { messages, tools, tool_choice, temperature, max_tokens, max_completion_tokens, reasoning_effort, reasoning, frequency_penalty, presence_penalty, seed, stop, top_p, ...rest } = params;
|
|
9
|
+
Object.assign(rest, parseReasoningOptions(reasoning_effort, reasoning));
|
|
10
|
+
return {
|
|
11
|
+
messages: convertToModelMessages(messages),
|
|
12
|
+
tools: convertToToolSet(tools),
|
|
13
|
+
toolChoice: convertToToolChoice(tool_choice),
|
|
14
|
+
temperature,
|
|
15
|
+
maxOutputTokens: max_completion_tokens ?? max_tokens,
|
|
16
|
+
frequencyPenalty: frequency_penalty,
|
|
17
|
+
presencePenalty: presence_penalty,
|
|
18
|
+
seed,
|
|
19
|
+
stopSequences: stop ? (Array.isArray(stop) ? stop : [stop]) : undefined,
|
|
20
|
+
topP: top_p,
|
|
21
|
+
providerOptions: {
|
|
22
|
+
unknown: rest,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function convertToModelMessages(messages) {
|
|
27
|
+
const modelMessages = [];
|
|
28
|
+
const toolById = indexToolMessages(messages);
|
|
29
|
+
for (const message of messages) {
|
|
30
|
+
if (message.role === "tool")
|
|
31
|
+
continue;
|
|
32
|
+
if (message.role === "system") {
|
|
33
|
+
modelMessages.push(message);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (message.role === "user") {
|
|
37
|
+
modelMessages.push(fromChatCompletionsUserMessage(message));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
modelMessages.push(fromChatCompletionsAssistantMessage(message));
|
|
41
|
+
const toolResult = fromChatCompletionsToolResultMessage(message, toolById);
|
|
42
|
+
if (toolResult)
|
|
43
|
+
modelMessages.push(toolResult);
|
|
44
|
+
}
|
|
45
|
+
return modelMessages;
|
|
46
|
+
}
|
|
47
|
+
function indexToolMessages(messages) {
|
|
48
|
+
const map = new Map();
|
|
49
|
+
for (const m of messages) {
|
|
50
|
+
if (m.role === "tool")
|
|
51
|
+
map.set(m.tool_call_id, m);
|
|
52
|
+
}
|
|
53
|
+
return map;
|
|
54
|
+
}
|
|
55
|
+
export function fromChatCompletionsUserMessage(message) {
|
|
56
|
+
return {
|
|
57
|
+
role: "user",
|
|
58
|
+
content: Array.isArray(message.content)
|
|
59
|
+
? fromChatCompletionsContent(message.content)
|
|
60
|
+
: message.content,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function fromChatCompletionsAssistantMessage(message) {
|
|
64
|
+
const { tool_calls, role, content, extra_content } = message;
|
|
65
|
+
if (!tool_calls?.length) {
|
|
66
|
+
const out = {
|
|
67
|
+
role: role,
|
|
68
|
+
content: content ?? "",
|
|
69
|
+
};
|
|
70
|
+
if (extra_content) {
|
|
71
|
+
out.providerOptions = extra_content;
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
role: role,
|
|
77
|
+
content: tool_calls.map((tc) => {
|
|
78
|
+
const { id, function: fn, extra_content } = tc;
|
|
79
|
+
const out = {
|
|
80
|
+
type: "tool-call",
|
|
81
|
+
toolCallId: id,
|
|
82
|
+
toolName: fn.name,
|
|
83
|
+
input: parseToolOutput(fn.arguments).value,
|
|
84
|
+
};
|
|
85
|
+
if (extra_content) {
|
|
86
|
+
out.providerOptions = extra_content;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function fromChatCompletionsToolResultMessage(message, toolById) {
|
|
93
|
+
const toolCalls = message.tool_calls ?? [];
|
|
94
|
+
if (toolCalls.length === 0)
|
|
95
|
+
return undefined;
|
|
96
|
+
const toolResultParts = [];
|
|
97
|
+
for (const tc of toolCalls) {
|
|
98
|
+
const toolMsg = toolById.get(tc.id);
|
|
99
|
+
if (!toolMsg)
|
|
100
|
+
continue;
|
|
101
|
+
toolResultParts.push({
|
|
102
|
+
type: "tool-result",
|
|
103
|
+
toolCallId: tc.id,
|
|
104
|
+
toolName: tc.function.name,
|
|
105
|
+
output: parseToolOutput(toolMsg.content),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return toolResultParts.length > 0 ? { role: "tool", content: toolResultParts } : undefined;
|
|
109
|
+
}
|
|
110
|
+
export function fromChatCompletionsContent(content) {
|
|
111
|
+
return content.map((part) => {
|
|
112
|
+
if (part.type === "image_url") {
|
|
113
|
+
const url = part.image_url.url;
|
|
114
|
+
if (url.startsWith("data:")) {
|
|
115
|
+
const { mimeType, base64Data } = parseDataUrl(url);
|
|
116
|
+
return mimeType.startsWith("image/")
|
|
117
|
+
? {
|
|
118
|
+
type: "image",
|
|
119
|
+
image: convertBase64ToUint8Array(base64Data),
|
|
120
|
+
mediaType: mimeType,
|
|
121
|
+
}
|
|
122
|
+
: {
|
|
123
|
+
type: "file",
|
|
124
|
+
data: convertBase64ToUint8Array(base64Data),
|
|
125
|
+
mediaType: mimeType,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
type: "image",
|
|
130
|
+
image: new URL(url),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (part.type === "file") {
|
|
134
|
+
let { data, media_type, filename } = part.file;
|
|
135
|
+
return media_type.startsWith("image/")
|
|
136
|
+
? {
|
|
137
|
+
type: "image",
|
|
138
|
+
image: convertBase64ToUint8Array(data),
|
|
139
|
+
mediaType: media_type,
|
|
140
|
+
}
|
|
141
|
+
: {
|
|
142
|
+
type: "file",
|
|
143
|
+
data: convertBase64ToUint8Array(data),
|
|
144
|
+
filename,
|
|
145
|
+
mediaType: media_type,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return part;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
export const convertToToolSet = (tools) => {
|
|
152
|
+
if (!tools) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const toolSet = {};
|
|
156
|
+
for (const t of tools) {
|
|
157
|
+
toolSet[t.function.name] = tool({
|
|
158
|
+
description: t.function.description,
|
|
159
|
+
inputSchema: jsonSchema(t.function.parameters),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return toolSet;
|
|
163
|
+
};
|
|
164
|
+
export const convertToToolChoice = (toolChoice) => {
|
|
165
|
+
if (!toolChoice) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
if (toolChoice === "none" || toolChoice === "auto" || toolChoice === "required") {
|
|
169
|
+
return toolChoice;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
type: "tool",
|
|
173
|
+
toolName: toolChoice.function.name,
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
function parseToolOutput(content) {
|
|
177
|
+
try {
|
|
178
|
+
return { type: "json", value: JSON.parse(content) };
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return { type: "text", value: content };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function parseDataUrl(url) {
|
|
185
|
+
const commaIndex = url.indexOf(",");
|
|
186
|
+
if (commaIndex <= "data:".length || commaIndex === url.length - 1) {
|
|
187
|
+
throw new GatewayError("Invalid data URL: missing metadata or data", 400);
|
|
188
|
+
}
|
|
189
|
+
const metadata = url.slice("data:".length, commaIndex);
|
|
190
|
+
const base64Data = url.slice(commaIndex + 1);
|
|
191
|
+
const semicolonIndex = metadata.indexOf(";");
|
|
192
|
+
const mimeType = (semicolonIndex === -1 ? metadata : metadata.slice(0, semicolonIndex)).trim();
|
|
193
|
+
if (!mimeType) {
|
|
194
|
+
throw new GatewayError("Invalid data URL: missing MIME type", 400);
|
|
195
|
+
}
|
|
196
|
+
return { mimeType, base64Data };
|
|
197
|
+
}
|
|
198
|
+
function parseReasoningOptions(reasoning_effort, reasoning) {
|
|
199
|
+
const effort = reasoning?.effort ?? reasoning_effort;
|
|
200
|
+
const max_tokens = reasoning?.max_tokens;
|
|
201
|
+
if (reasoning?.enabled === false || effort === "none") {
|
|
202
|
+
return { reasoning: { enabled: false }, reasoning_effort: "none" };
|
|
203
|
+
}
|
|
204
|
+
if (!reasoning && effort === undefined)
|
|
205
|
+
return {};
|
|
206
|
+
const out = { reasoning: {} };
|
|
207
|
+
if (effort) {
|
|
208
|
+
out.reasoning.enabled = true;
|
|
209
|
+
out.reasoning.effort = effort;
|
|
210
|
+
out.reasoning_effort = effort;
|
|
211
|
+
}
|
|
212
|
+
if (max_tokens) {
|
|
213
|
+
out.reasoning.enabled = true;
|
|
214
|
+
out.reasoning.max_tokens = max_tokens;
|
|
215
|
+
}
|
|
216
|
+
if (out.reasoning.enabled) {
|
|
217
|
+
out.reasoning.exclude = reasoning?.exclude;
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
// --- Response Flow ---
|
|
222
|
+
export function toChatCompletions(result, model) {
|
|
223
|
+
const finish_reason = toChatCompletionsFinishReason(result.finishReason);
|
|
224
|
+
return {
|
|
225
|
+
id: "chatcmpl-" + crypto.randomUUID(),
|
|
226
|
+
object: "chat.completion",
|
|
227
|
+
created: Math.floor(Date.now() / 1000),
|
|
228
|
+
model,
|
|
229
|
+
choices: [
|
|
230
|
+
{
|
|
231
|
+
index: 0,
|
|
232
|
+
message: toChatCompletionsAssistantMessage(result),
|
|
233
|
+
finish_reason,
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
usage: result.totalUsage ? toChatCompletionsUsage(result.totalUsage) : null,
|
|
237
|
+
provider_metadata: result.providerMetadata,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
export function toChatCompletionsResponse(result, model, responseInit) {
|
|
241
|
+
return toResponse(toChatCompletions(result, model), responseInit);
|
|
242
|
+
}
|
|
243
|
+
export function toChatCompletionsStream(result, model) {
|
|
244
|
+
return result.fullStream
|
|
245
|
+
.pipeThrough(new ChatCompletionsStream(model))
|
|
246
|
+
.pipeThrough(new JsonToSseTransformStream())
|
|
247
|
+
.pipeThrough(new TextEncoderStream());
|
|
248
|
+
}
|
|
249
|
+
export function toChatCompletionsStreamResponse(result, model, responseInit) {
|
|
250
|
+
return toResponse(toChatCompletionsStream(result, model), responseInit);
|
|
251
|
+
}
|
|
252
|
+
export class ChatCompletionsStream extends TransformStream {
|
|
253
|
+
constructor(model) {
|
|
254
|
+
const streamId = `chatcmpl-${crypto.randomUUID()}`;
|
|
255
|
+
const creationTime = Math.floor(Date.now() / 1000);
|
|
256
|
+
let toolCallIndexCounter = 0;
|
|
257
|
+
const createChunk = (delta, provider_metadata, finish_reason, usage) => {
|
|
258
|
+
if (provider_metadata) {
|
|
259
|
+
delta.extra_content = provider_metadata;
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
id: streamId,
|
|
263
|
+
object: "chat.completion.chunk",
|
|
264
|
+
created: creationTime,
|
|
265
|
+
model,
|
|
266
|
+
choices: [
|
|
267
|
+
{
|
|
268
|
+
index: 0,
|
|
269
|
+
delta,
|
|
270
|
+
finish_reason: finish_reason ?? null,
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
usage: usage ?? null,
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
super({
|
|
277
|
+
transform(part, controller) {
|
|
278
|
+
switch (part.type) {
|
|
279
|
+
case "text-delta": {
|
|
280
|
+
controller.enqueue(createChunk({ role: "assistant", content: part.text }, part.providerMetadata));
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case "reasoning-delta": {
|
|
284
|
+
controller.enqueue(createChunk({ reasoning_content: part.text }, part.providerMetadata));
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case "tool-call": {
|
|
288
|
+
const toolCall = toChatCompletionsToolCall(part.toolCallId, part.toolName, part.input, part.providerMetadata);
|
|
289
|
+
toolCall.index = toolCallIndexCounter++;
|
|
290
|
+
controller.enqueue(createChunk({
|
|
291
|
+
tool_calls: [toolCall],
|
|
292
|
+
}));
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
case "finish-step": {
|
|
296
|
+
controller.enqueue(createChunk({}, part.providerMetadata, toChatCompletionsFinishReason(part.finishReason), toChatCompletionsUsage(part.usage)));
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
case "finish": {
|
|
300
|
+
controller.enqueue(createChunk({}, undefined, toChatCompletionsFinishReason(part.finishReason), toChatCompletionsUsage(part.totalUsage)));
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case "error": {
|
|
304
|
+
const error = part.error;
|
|
305
|
+
controller.enqueue(toOpenAIError(error));
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
export const toChatCompletionsAssistantMessage = (result) => {
|
|
314
|
+
const message = {
|
|
315
|
+
role: "assistant",
|
|
316
|
+
content: null,
|
|
317
|
+
};
|
|
318
|
+
if (result.toolCalls && result.toolCalls.length > 0) {
|
|
319
|
+
message.tool_calls = result.toolCalls.map((toolCall) => toChatCompletionsToolCall(toolCall.toolCallId, toolCall.toolName, toolCall.input, toolCall.providerMetadata));
|
|
320
|
+
}
|
|
321
|
+
for (const part of result.content) {
|
|
322
|
+
if (part.type === "text") {
|
|
323
|
+
message.content = part.text;
|
|
324
|
+
if (part.providerMetadata) {
|
|
325
|
+
message.extra_content = part.providerMetadata;
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (result.reasoningText) {
|
|
331
|
+
message.reasoning_content = result.reasoningText;
|
|
332
|
+
}
|
|
333
|
+
return message;
|
|
334
|
+
};
|
|
335
|
+
export function toChatCompletionsUsage(usage) {
|
|
336
|
+
const out = {};
|
|
337
|
+
const prompt = usage.inputTokens;
|
|
338
|
+
if (prompt !== undefined)
|
|
339
|
+
out.prompt_tokens = prompt;
|
|
340
|
+
const completion = usage.outputTokens;
|
|
341
|
+
if (completion !== undefined)
|
|
342
|
+
out.completion_tokens = completion;
|
|
343
|
+
if (prompt !== undefined || completion !== undefined || usage.totalTokens !== undefined) {
|
|
344
|
+
out.total_tokens = usage.totalTokens ?? (prompt ?? 0) + (completion ?? 0);
|
|
345
|
+
}
|
|
346
|
+
const reasoning = usage.outputTokenDetails?.reasoningTokens;
|
|
347
|
+
if (reasoning !== undefined)
|
|
348
|
+
out.completion_tokens_details = { reasoning_tokens: reasoning };
|
|
349
|
+
const cached = usage.inputTokenDetails?.cacheReadTokens;
|
|
350
|
+
if (cached !== undefined)
|
|
351
|
+
out.prompt_tokens_details = { cached_tokens: cached };
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
export function toChatCompletionsToolCall(id, name, args, providerMetadata) {
|
|
355
|
+
const out = {
|
|
356
|
+
id,
|
|
357
|
+
type: "function",
|
|
358
|
+
function: {
|
|
359
|
+
name,
|
|
360
|
+
arguments: typeof args === "string" ? args : JSON.stringify(args),
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
if (providerMetadata) {
|
|
364
|
+
out.extra_content = providerMetadata;
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
export const toChatCompletionsFinishReason = (finishReason) => {
|
|
369
|
+
if (finishReason === "error" || finishReason === "other") {
|
|
370
|
+
return "stop";
|
|
371
|
+
}
|
|
372
|
+
return finishReason.replaceAll("-", "_");
|
|
373
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { generateText, streamText, wrapLanguageModel } from "ai";
|
|
2
|
+
import * as z from "zod/mini";
|
|
3
|
+
import { GatewayError } from "../../errors/gateway";
|
|
4
|
+
import { winterCgHandler } from "../../lifecycle";
|
|
5
|
+
import { logger } from "../../logger";
|
|
6
|
+
import { modelMiddlewareMatcher } from "../../middleware/matcher";
|
|
7
|
+
import { resolveProvider } from "../../providers/registry";
|
|
8
|
+
import { prepareForwardHeaders } from "../../utils/request";
|
|
9
|
+
import { convertToTextCallOptions, toChatCompletions, toChatCompletionsStream } from "./converters";
|
|
10
|
+
import { ChatCompletionsBodySchema } from "./schema";
|
|
11
|
+
export const chatCompletions = (config) => {
|
|
12
|
+
const hooks = config.hooks;
|
|
13
|
+
const handler = async (ctx) => {
|
|
14
|
+
// Guard: enforce HTTP method early.
|
|
15
|
+
if (!ctx.request || ctx.request.method !== "POST") {
|
|
16
|
+
throw new GatewayError("Method Not Allowed", 405);
|
|
17
|
+
}
|
|
18
|
+
// Parse + validate input.
|
|
19
|
+
let body;
|
|
20
|
+
try {
|
|
21
|
+
body = await ctx.request.json();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new GatewayError("Invalid JSON", 400);
|
|
25
|
+
}
|
|
26
|
+
const parsed = ChatCompletionsBodySchema.safeParse(body);
|
|
27
|
+
if (!parsed.success) {
|
|
28
|
+
throw new GatewayError(z.prettifyError(parsed.error), 400);
|
|
29
|
+
}
|
|
30
|
+
ctx.body = parsed.data;
|
|
31
|
+
// Resolve model + provider (hooks may override defaults).
|
|
32
|
+
let inputs, stream;
|
|
33
|
+
({ model: ctx.modelId, stream, ...inputs } = parsed.data);
|
|
34
|
+
ctx.resolvedModelId =
|
|
35
|
+
(await hooks?.resolveModelId?.(ctx)) ?? ctx.modelId;
|
|
36
|
+
logger.debug(`[chat] resolved ${ctx.modelId} to ${ctx.resolvedModelId}`);
|
|
37
|
+
ctx.operation = "text";
|
|
38
|
+
const override = await hooks?.resolveProvider?.(ctx);
|
|
39
|
+
ctx.provider =
|
|
40
|
+
override ??
|
|
41
|
+
resolveProvider({
|
|
42
|
+
providers: ctx.providers,
|
|
43
|
+
models: ctx.models,
|
|
44
|
+
modelId: ctx.resolvedModelId,
|
|
45
|
+
operation: ctx.operation,
|
|
46
|
+
});
|
|
47
|
+
const languageModel = ctx.provider.languageModel(ctx.resolvedModelId);
|
|
48
|
+
ctx.resolvedProviderId = languageModel.provider;
|
|
49
|
+
logger.debug(`[chat] using ${languageModel.provider} for ${ctx.resolvedModelId}`);
|
|
50
|
+
// Convert inputs to AI SDK call options.
|
|
51
|
+
const textOptions = convertToTextCallOptions(inputs);
|
|
52
|
+
logger.trace({
|
|
53
|
+
requestId: ctx.request.headers.get("x-request-id"),
|
|
54
|
+
options: textOptions,
|
|
55
|
+
}, "[chat] AI SDK options");
|
|
56
|
+
// Build middleware chain (model -> forward params -> provider).
|
|
57
|
+
const languageModelWithMiddleware = wrapLanguageModel({
|
|
58
|
+
model: languageModel,
|
|
59
|
+
middleware: modelMiddlewareMatcher.for(ctx.resolvedModelId, languageModel.provider),
|
|
60
|
+
});
|
|
61
|
+
// Execute request (streaming vs. non-streaming).
|
|
62
|
+
if (stream) {
|
|
63
|
+
const result = streamText({
|
|
64
|
+
model: languageModelWithMiddleware,
|
|
65
|
+
headers: prepareForwardHeaders(ctx.request),
|
|
66
|
+
// No abort signal here, otherwise we can't detect upstream from client cancellations
|
|
67
|
+
// abortSignal: ctx.request.signal,
|
|
68
|
+
onError: ({ error }) => {
|
|
69
|
+
logger.error(error instanceof Error ? error : new Error(String(error)), {
|
|
70
|
+
requestId: ctx.request.headers.get("x-request-id"),
|
|
71
|
+
});
|
|
72
|
+
throw error;
|
|
73
|
+
},
|
|
74
|
+
onAbort: () => {
|
|
75
|
+
throw new DOMException("Upstream failed", "AbortError");
|
|
76
|
+
},
|
|
77
|
+
experimental_include: {
|
|
78
|
+
requestBody: false,
|
|
79
|
+
},
|
|
80
|
+
includeRawChunks: false,
|
|
81
|
+
...textOptions,
|
|
82
|
+
});
|
|
83
|
+
return toChatCompletionsStream(result, ctx.modelId);
|
|
84
|
+
}
|
|
85
|
+
const result = await generateText({
|
|
86
|
+
model: languageModelWithMiddleware,
|
|
87
|
+
headers: prepareForwardHeaders(ctx.request),
|
|
88
|
+
abortSignal: ctx.request.signal,
|
|
89
|
+
experimental_include: {
|
|
90
|
+
requestBody: false,
|
|
91
|
+
responseBody: false,
|
|
92
|
+
},
|
|
93
|
+
...textOptions,
|
|
94
|
+
});
|
|
95
|
+
logger.trace({ requestId: ctx.request.headers.get("x-request-id"), result }, "[chat] AI SDK result");
|
|
96
|
+
return toChatCompletions(result, ctx.modelId);
|
|
97
|
+
};
|
|
98
|
+
return { handler: winterCgHandler(handler, config) };
|
|
99
|
+
};
|