@gajae-code/ai 0.18.11 → 0.18.12
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 +68 -0
- package/README.md +1 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +30 -0
- package/dist/types/auth-storage.d.ts +80 -8
- package/dist/types/bedrock-claude-cache-policy.d.ts +1 -1
- package/dist/types/model-cache.d.ts +6 -4
- package/dist/types/model-manager.d.ts +2 -0
- package/dist/types/openai-completions-compat.d.ts +2 -1
- package/dist/types/providers/anthropic.d.ts +10 -0
- package/dist/types/providers/aws-credential-config.d.ts +5 -0
- package/dist/types/providers/google-auth.d.ts +7 -0
- package/dist/types/providers/google-gemini-headers.d.ts +10 -5
- package/dist/types/providers/kiro-api-key.d.ts +5 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +2 -1
- package/dist/types/providers/openai-completions.d.ts +1 -1
- package/dist/types/providers/openai-responses-shared.d.ts +2 -1
- package/dist/types/stream.d.ts +33 -1
- package/dist/types/types.d.ts +26 -3
- package/dist/types/usage.d.ts +3 -0
- package/dist/types/utils/discovery/antigravity.d.ts +11 -0
- package/dist/types/utils/idle-iterator.d.ts +3 -2
- package/dist/types/utils/oauth/types.d.ts +9 -0
- package/package.json +4 -4
- package/src/auth-broker/remote-store.ts +62 -11
- package/src/auth-broker/wire-schemas.ts +2 -0
- package/src/auth-gateway/server.ts +3 -2
- package/src/auth-storage.ts +647 -194
- package/src/bedrock-claude-cache-policy.d.ts +1 -1
- package/src/bedrock-claude-cache-policy.ts +2 -2
- package/src/model-cache.ts +45 -10
- package/src/model-manager.ts +18 -6
- package/src/model-thinking.ts +37 -7
- package/src/models.json +2931 -1670
- package/src/openai-completions-compat.d.ts +2 -1
- package/src/openai-completions-compat.ts +5 -0
- package/src/provider-models/google.ts +7 -1
- package/src/provider-models/openai-compat.ts +26 -0
- package/src/providers/amazon-bedrock.ts +20 -11
- package/src/providers/anthropic.d.ts +10 -0
- package/src/providers/anthropic.ts +139 -4
- package/src/providers/aws-credential-config.d.ts +5 -0
- package/src/providers/aws-credential-config.ts +29 -1
- package/src/providers/google-auth.d.ts +7 -0
- package/src/providers/google-auth.ts +165 -4
- package/src/providers/google-gemini-headers.d.ts +10 -5
- package/src/providers/google-gemini-headers.ts +11 -6
- package/src/providers/google-vertex.ts +2 -4
- package/src/providers/kiro-api-key.d.ts +5 -0
- package/src/providers/kiro-api-key.ts +379 -68
- package/src/providers/ollama.ts +19 -9
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-chat-server-schema.ts +1 -1
- package/src/providers/openai-chat-server.ts +14 -2
- package/src/providers/openai-codex-responses.ts +21 -14
- package/src/providers/openai-completions.d.ts +1 -1
- package/src/providers/openai-completions.ts +65 -4
- package/src/providers/openai-responses-server.ts +10 -1
- package/src/providers/openai-responses-shared.d.ts +2 -1
- package/src/providers/openai-responses-shared.ts +42 -1
- package/src/providers/openai-responses.ts +51 -10
- package/src/providers/pi-native-server.ts +1 -1
- package/src/stream.d.ts +33 -1
- package/src/stream.ts +107 -13
- package/src/types.d.ts +26 -3
- package/src/types.ts +55 -2
- package/src/usage/claude.ts +4 -2
- package/src/usage.ts +3 -0
- package/src/utils/discovery/antigravity.ts +144 -2
- package/src/utils/idle-iterator.d.ts +3 -2
- package/src/utils/idle-iterator.ts +30 -10
- package/src/utils/oauth/anthropic.ts +23 -22
- package/src/utils/oauth/api-key-validation.ts +34 -11
- package/src/utils/oauth/types.d.ts +9 -0
- package/src/utils/oauth/types.ts +9 -0
- package/src/utils/parse-bind.d.ts +35 -0
package/src/providers/ollama.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "../utils/tool-choice-capability";
|
|
29
29
|
import { flagTruncatedToolCalls } from "./openai-responses-shared";
|
|
30
30
|
import { transformMessages } from "./transform-messages";
|
|
31
|
+
import { joinTextWithImagePlaceholder } from "./vision-guard";
|
|
31
32
|
|
|
32
33
|
export interface OllamaChatOptions extends StreamOptions {
|
|
33
34
|
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
@@ -148,7 +149,10 @@ function selectToolsForToolChoice(tools: Tool[] | undefined, toolChoice: ToolCho
|
|
|
148
149
|
return [];
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
function toPlainContent(
|
|
152
|
+
function toPlainContent(
|
|
153
|
+
content: string | Array<{ type: "text" | "image"; text?: string; data?: string }>,
|
|
154
|
+
supportsImages: boolean,
|
|
155
|
+
): {
|
|
152
156
|
content: string;
|
|
153
157
|
images?: string[];
|
|
154
158
|
} {
|
|
@@ -157,31 +161,36 @@ function toPlainContent(content: string | Array<{ type: "text" | "image"; text?:
|
|
|
157
161
|
}
|
|
158
162
|
const textParts: string[] = [];
|
|
159
163
|
const images: string[] = [];
|
|
164
|
+
let omittedImages = false;
|
|
160
165
|
for (const block of content) {
|
|
161
166
|
if (block.type === "text" && typeof block.text === "string") {
|
|
162
167
|
textParts.push(block.text);
|
|
163
168
|
}
|
|
164
|
-
if (block.type === "image"
|
|
165
|
-
|
|
169
|
+
if (block.type === "image") {
|
|
170
|
+
if (supportsImages && typeof block.data === "string") {
|
|
171
|
+
images.push(block.data);
|
|
172
|
+
} else if (!supportsImages) {
|
|
173
|
+
omittedImages = true;
|
|
174
|
+
}
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
177
|
return {
|
|
169
|
-
content: textParts.join("\n"),
|
|
178
|
+
content: joinTextWithImagePlaceholder(textParts.join("\n"), omittedImages),
|
|
170
179
|
...(images.length > 0 ? { images } : {}),
|
|
171
180
|
};
|
|
172
181
|
}
|
|
173
182
|
|
|
174
|
-
function convertMessage(message: Message): OllamaMessage {
|
|
183
|
+
function convertMessage(message: Message, supportsImages: boolean): OllamaMessage {
|
|
175
184
|
if (message.role === "user") {
|
|
176
|
-
const converted = toPlainContent(message.content as UserMessage["content"]);
|
|
185
|
+
const converted = toPlainContent(message.content as UserMessage["content"], supportsImages);
|
|
177
186
|
return { role: "user", ...converted };
|
|
178
187
|
}
|
|
179
188
|
if (message.role === "developer") {
|
|
180
|
-
const converted = toPlainContent(message.content as DeveloperMessage["content"]);
|
|
189
|
+
const converted = toPlainContent(message.content as DeveloperMessage["content"], supportsImages);
|
|
181
190
|
return { role: "system", ...converted };
|
|
182
191
|
}
|
|
183
192
|
if (message.role === "toolResult") {
|
|
184
|
-
const converted = toPlainContent(message.content as ToolResultMessage["content"]);
|
|
193
|
+
const converted = toPlainContent(message.content as ToolResultMessage["content"], supportsImages);
|
|
185
194
|
return {
|
|
186
195
|
role: "tool",
|
|
187
196
|
tool_name: message.toolName,
|
|
@@ -233,8 +242,9 @@ function convertMessages(model: Model<"ollama-chat">, context: Context): OllamaM
|
|
|
233
242
|
}
|
|
234
243
|
messages.push(...context.messages);
|
|
235
244
|
const isCloud = model.provider === "ollama-cloud";
|
|
245
|
+
const supportsImages = model.input.includes("image");
|
|
236
246
|
return transformMessages(messages, model).map(msg => {
|
|
237
|
-
const converted = convertMessage(msg);
|
|
247
|
+
const converted = convertMessage(msg, supportsImages);
|
|
238
248
|
// Ollama cloud rejects requests when assistant history messages contain the `thinking`
|
|
239
249
|
// field — it's valid in model responses but not accepted as a history input. Strip it
|
|
240
250
|
// to prevent HTTP 400 errors. Local Ollama instances are unaffected.
|
|
@@ -223,7 +223,7 @@ export const openaiChatRequestSchema = z.object({
|
|
|
223
223
|
user: nullableOptional(z.string()),
|
|
224
224
|
reasoning_effort: nullableOptional(z.enum(["minimal", "low", "medium", "high", "xhigh", "max"])),
|
|
225
225
|
parallel_tool_calls: nullableOptional(z.boolean()),
|
|
226
|
-
service_tier: nullableOptional(z.enum(["auto", "default", "flex", "scale", "priority"])),
|
|
226
|
+
service_tier: nullableOptional(z.enum(["auto", "default", "flex", "scale", "priority", "ultrafast"])),
|
|
227
227
|
metadata: nullableOptional(z.record(z.string(), z.unknown())),
|
|
228
228
|
|
|
229
229
|
// ── Accept-and-ignore passthroughs ─────────────────────────────────────
|
|
@@ -46,7 +46,14 @@ function isReasoningEffort(value: unknown): value is ReasoningEffort {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function isServiceTier(value: unknown): value is ResolvedServiceTier {
|
|
49
|
-
return
|
|
49
|
+
return (
|
|
50
|
+
value === "auto" ||
|
|
51
|
+
value === "default" ||
|
|
52
|
+
value === "flex" ||
|
|
53
|
+
value === "scale" ||
|
|
54
|
+
value === "priority" ||
|
|
55
|
+
value === "ultrafast"
|
|
56
|
+
);
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
// ---------------------------------------------------------------------------
|
|
@@ -399,6 +406,7 @@ export function encodeResponse(message: AssistantMessage, requestedModelId: stri
|
|
|
399
406
|
logprobs: null,
|
|
400
407
|
},
|
|
401
408
|
],
|
|
409
|
+
...(message.serviceTier ? { service_tier: message.serviceTier } : {}),
|
|
402
410
|
usage: buildUsage(message),
|
|
403
411
|
};
|
|
404
412
|
}
|
|
@@ -536,6 +544,7 @@ export function encodeStream(
|
|
|
536
544
|
model: requestedModelId,
|
|
537
545
|
system_fingerprint: null,
|
|
538
546
|
choices: [],
|
|
547
|
+
...(message.serviceTier ? { service_tier: message.serviceTier } : {}),
|
|
539
548
|
usage: buildUsage(message),
|
|
540
549
|
});
|
|
541
550
|
};
|
|
@@ -671,7 +680,10 @@ export function encodeStream(
|
|
|
671
680
|
: hasToolCalls
|
|
672
681
|
? "tool_calls"
|
|
673
682
|
: "stop";
|
|
674
|
-
writeSse(controller,
|
|
683
|
+
writeSse(controller, {
|
|
684
|
+
...baseChunk({}, finishReason),
|
|
685
|
+
...(event.message.serviceTier ? { service_tier: event.message.serviceTier } : {}),
|
|
686
|
+
});
|
|
675
687
|
if (includeUsage) writeUsage(controller, event.message);
|
|
676
688
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
677
689
|
controller.close();
|
|
@@ -32,7 +32,9 @@ import {
|
|
|
32
32
|
type Context,
|
|
33
33
|
type FetchImpl,
|
|
34
34
|
type Model,
|
|
35
|
+
modelSupportsUltrafastTier,
|
|
35
36
|
type ProviderSessionState,
|
|
37
|
+
parseResolvedServiceTier,
|
|
36
38
|
resolveServiceTier,
|
|
37
39
|
type ServiceTier,
|
|
38
40
|
type StreamFunction,
|
|
@@ -774,15 +776,15 @@ function getCodexUserAgent(): string {
|
|
|
774
776
|
return formatCodexUserAgent(os.platform(), os.release(), os.arch());
|
|
775
777
|
}
|
|
776
778
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
serviceTier: ServiceTier | "default" | undefined,
|
|
780
|
-
): number {
|
|
779
|
+
/** ChatGPT Codex included-usage multipliers; API token pricing uses separate provider paths. */
|
|
780
|
+
function getCodexServiceTierCostMultiplier(serviceTier: ServiceTier | "default" | undefined): number {
|
|
781
781
|
switch (serviceTier) {
|
|
782
782
|
case "flex":
|
|
783
783
|
return 0.5;
|
|
784
784
|
case "priority":
|
|
785
|
-
return
|
|
785
|
+
return 2.5;
|
|
786
|
+
case "ultrafast":
|
|
787
|
+
return 8;
|
|
786
788
|
default:
|
|
787
789
|
return 1;
|
|
788
790
|
}
|
|
@@ -795,23 +797,19 @@ function resolveCodexCostServiceTier(res: unknown, req?: unknown): ServiceTier |
|
|
|
795
797
|
case "flex":
|
|
796
798
|
case "scale":
|
|
797
799
|
case "priority":
|
|
800
|
+
case "ultrafast":
|
|
798
801
|
return res;
|
|
799
802
|
default:
|
|
800
|
-
if (req === "flex" || req === "priority") {
|
|
803
|
+
if (req === "flex" || req === "priority" || req === "ultrafast") {
|
|
801
804
|
return req;
|
|
802
805
|
}
|
|
803
806
|
return "default";
|
|
804
807
|
}
|
|
805
808
|
}
|
|
806
809
|
|
|
807
|
-
function applyCodexServiceTierPricing(
|
|
808
|
-
model: Pick<Model<"openai-codex-responses">, "id">,
|
|
809
|
-
usage: AssistantMessage["usage"],
|
|
810
|
-
resTier: unknown,
|
|
811
|
-
reqTier: unknown,
|
|
812
|
-
): void {
|
|
810
|
+
function applyCodexServiceTierPricing(usage: AssistantMessage["usage"], resTier: unknown, reqTier: unknown): void {
|
|
813
811
|
const resolvedTier = resolveCodexCostServiceTier(resTier, reqTier);
|
|
814
|
-
const multiplier = getCodexServiceTierCostMultiplier(
|
|
812
|
+
const multiplier = getCodexServiceTierCostMultiplier(resolvedTier);
|
|
815
813
|
if (multiplier === 1) return;
|
|
816
814
|
usage.cost.input *= multiplier;
|
|
817
815
|
usage.cost.output *= multiplier;
|
|
@@ -988,6 +986,12 @@ async function buildTransformedCodexRequestBody(
|
|
|
988
986
|
const resolvedServiceTier = resolveServiceTier(options?.serviceTier, model.provider);
|
|
989
987
|
if (resolvedServiceTier === "flex" || resolvedServiceTier === "scale" || resolvedServiceTier === "priority") {
|
|
990
988
|
params.service_tier = resolvedServiceTier;
|
|
989
|
+
} else if (resolvedServiceTier === "ultrafast") {
|
|
990
|
+
// Gate ultrafast to models that explicitly support it
|
|
991
|
+
if (modelSupportsUltrafastTier(model)) {
|
|
992
|
+
params.service_tier = "ultrafast";
|
|
993
|
+
}
|
|
994
|
+
// else: silently omit ultrafast if model doesn't support it (fail-closed)
|
|
991
995
|
}
|
|
992
996
|
if (context.tools && context.tools.length > 0) {
|
|
993
997
|
params.tools = convertOpenAICodexResponsesTools(context.tools, model);
|
|
@@ -2301,7 +2305,10 @@ function handleResponseCompleted(
|
|
|
2301
2305
|
}
|
|
2302
2306
|
|
|
2303
2307
|
calculateCost(model, output.usage);
|
|
2304
|
-
|
|
2308
|
+
output.serviceTier =
|
|
2309
|
+
parseResolvedServiceTier(response?.service_tier) ??
|
|
2310
|
+
parseResolvedServiceTier(runtime.requestBodyForState.service_tier);
|
|
2311
|
+
applyCodexServiceTierPricing(output.usage, response?.service_tier, runtime.requestBodyForState.service_tier);
|
|
2305
2312
|
output.stopReason = mapOpenAIResponsesStopReason(response?.status as OpenAI.Responses.ResponseStatus | undefined);
|
|
2306
2313
|
// A response cut short for length may have stopped mid-tool-call. Flag any
|
|
2307
2314
|
// call that never received its `output_item.done` so the agent loop rejects
|
|
@@ -33,7 +33,7 @@ export interface OpenAICompletionsOptions extends StreamOptions {
|
|
|
33
33
|
repetitionGuard?: RepetitionGuardOptions;
|
|
34
34
|
}
|
|
35
35
|
export declare const streamOpenAICompletions: StreamFunction<"openai-completions">;
|
|
36
|
-
export declare function parseChunkUsage(rawUsage: object, model: Model<"openai-completions">, premiumRequests: number | undefined): AssistantMessage["usage"];
|
|
36
|
+
export declare function parseChunkUsage(rawUsage: object, model: Model<"openai-completions">, premiumRequests: number | undefined, serviceTier?: ServiceTier): AssistantMessage["usage"];
|
|
37
37
|
export declare function convertMessages(model: Model<"openai-completions">, context: Context, compat: ResolvedOpenAICompat): ChatCompletionMessageParam[];
|
|
38
38
|
/**
|
|
39
39
|
* Detect compatibility settings from provider and baseUrl for known providers.
|
|
@@ -33,8 +33,10 @@ import {
|
|
|
33
33
|
type Message,
|
|
34
34
|
type MessageAttribution,
|
|
35
35
|
type Model,
|
|
36
|
+
modelSupportsUltrafastTier,
|
|
36
37
|
type OpenAICompat,
|
|
37
38
|
type ProviderSessionState,
|
|
39
|
+
parseResolvedServiceTier,
|
|
38
40
|
type RepetitionGuardOptions,
|
|
39
41
|
resolveServiceTier,
|
|
40
42
|
type ServiceTier,
|
|
@@ -660,7 +662,9 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
660
662
|
|
|
661
663
|
try {
|
|
662
664
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
663
|
-
const idleTimeoutMs =
|
|
665
|
+
const idleTimeoutMs =
|
|
666
|
+
options?.streamIdleTimeoutMs ??
|
|
667
|
+
getOpenAIStreamIdleTimeoutMs(model.provider, model.id, model.streamIdleTimeoutMs);
|
|
664
668
|
const {
|
|
665
669
|
client,
|
|
666
670
|
copilotPremiumRequests,
|
|
@@ -685,6 +689,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
685
689
|
options?.providerSessionId,
|
|
686
690
|
options?.maxRetryDelayMs,
|
|
687
691
|
options?.attemptScope,
|
|
692
|
+
options?.streamIdleTimeoutMs,
|
|
688
693
|
);
|
|
689
694
|
const premiumRequestsTotal = copilotPremiumRequests;
|
|
690
695
|
getCapturedErrorResponse = captureErrorResponse;
|
|
@@ -696,6 +701,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
696
701
|
);
|
|
697
702
|
let disableStrictTools = providerSessionState?.strictToolsDisabled ?? false;
|
|
698
703
|
let strictFallbackErrorMessage: string | undefined;
|
|
704
|
+
let effectiveServiceTier: ServiceTier | undefined;
|
|
699
705
|
const createCompletionsStream = async (toolStrictModeOverride?: ToolStrictModeOverride) => {
|
|
700
706
|
clearCapturedErrorResponse();
|
|
701
707
|
const effectiveToolStrictModeOverride = disableStrictTools ? "none" : toolStrictModeOverride;
|
|
@@ -717,6 +723,9 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
717
723
|
if (replacementPayload !== undefined) {
|
|
718
724
|
params = replacementPayload as typeof params;
|
|
719
725
|
}
|
|
726
|
+
// Capture the service tier for use in cost calculations
|
|
727
|
+
effectiveServiceTier = (params as Record<string, unknown>).service_tier as ServiceTier | undefined;
|
|
728
|
+
output.serviceTier = parseResolvedServiceTier(effectiveServiceTier);
|
|
720
729
|
rawRequestDump = {
|
|
721
730
|
provider: model.provider,
|
|
722
731
|
api: output.api,
|
|
@@ -1149,7 +1158,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
1149
1158
|
let hasExplicitUsageReport = false;
|
|
1150
1159
|
let hasNonzeroUsageEvidence = false;
|
|
1151
1160
|
const applyUsage = (rawUsage: object): void => {
|
|
1152
|
-
const usage = parseChunkUsage(rawUsage, model, premiumRequestsTotal);
|
|
1161
|
+
const usage = parseChunkUsage(rawUsage, model, premiumRequestsTotal, effectiveServiceTier);
|
|
1153
1162
|
const hasExplicitTotal = getOptionalNumberProperty(rawUsage, "total_tokens") !== undefined;
|
|
1154
1163
|
const hasExplicitComponents =
|
|
1155
1164
|
getOptionalNumberProperty(rawUsage, "prompt_tokens") !== undefined &&
|
|
@@ -1187,6 +1196,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
1187
1196
|
// `choices`-less or malformed chunks after a trip never spends the
|
|
1188
1197
|
// budget and the request hangs open (#5627 review r5).
|
|
1189
1198
|
if (chunk && typeof chunk === "object") {
|
|
1199
|
+
const responseServiceTier = parseResolvedServiceTier(Reflect.get(chunk, "service_tier"));
|
|
1200
|
+
if (responseServiceTier !== undefined) {
|
|
1201
|
+
effectiveServiceTier = responseServiceTier;
|
|
1202
|
+
output.serviceTier = responseServiceTier;
|
|
1203
|
+
}
|
|
1190
1204
|
// OpenAI documents ChatCompletionChunk.id as the unique chat completion identifier,
|
|
1191
1205
|
// and each chunk in a streamed completion carries the same id.
|
|
1192
1206
|
output.responseId ||= chunk.id;
|
|
@@ -1540,6 +1554,7 @@ async function createClient(
|
|
|
1540
1554
|
providerSessionId?: string,
|
|
1541
1555
|
maxRetryDelayMs?: number,
|
|
1542
1556
|
attemptScope?: import("../types.js").AttemptScopeRef,
|
|
1557
|
+
effectiveIdleTimeoutOverride?: number,
|
|
1543
1558
|
): Promise<{
|
|
1544
1559
|
client: OpenAI;
|
|
1545
1560
|
copilotPremiumRequests: number | undefined;
|
|
@@ -1686,7 +1701,13 @@ async function createClient(
|
|
|
1686
1701
|
// The OpenAI SDK's default is 10 minutes per attempt × `maxRetries`, which
|
|
1687
1702
|
// turns a stalled-before-headers fetch into a multi-minute hang invisible
|
|
1688
1703
|
// to the agent loop (the iterator watchdog only arms AFTER `create()` returns).
|
|
1689
|
-
const sdkTimeoutMs = resolveOpenAISdkRequestTimeoutMs(
|
|
1704
|
+
const sdkTimeoutMs = resolveOpenAISdkRequestTimeoutMs(
|
|
1705
|
+
model.provider,
|
|
1706
|
+
streamFirstEventTimeoutOverride,
|
|
1707
|
+
model.id,
|
|
1708
|
+
model.streamIdleTimeoutMs,
|
|
1709
|
+
effectiveIdleTimeoutOverride,
|
|
1710
|
+
);
|
|
1690
1711
|
return {
|
|
1691
1712
|
client: new OpenAI({
|
|
1692
1713
|
apiKey,
|
|
@@ -1792,10 +1813,23 @@ function buildParams(
|
|
|
1792
1813
|
if (options?.frequencyPenalty !== undefined) {
|
|
1793
1814
|
params.frequency_penalty = options.frequencyPenalty;
|
|
1794
1815
|
}
|
|
1795
|
-
if (
|
|
1816
|
+
if (
|
|
1817
|
+
shouldSendServiceTier(
|
|
1818
|
+
options?.serviceTier,
|
|
1819
|
+
model.provider,
|
|
1820
|
+
compat.supportsServiceTier === true,
|
|
1821
|
+
modelSupportsUltrafastTier(model),
|
|
1822
|
+
)
|
|
1823
|
+
) {
|
|
1796
1824
|
const resolved = resolveServiceTier(options?.serviceTier, model.provider);
|
|
1797
1825
|
if (resolved === "flex" || resolved === "scale" || resolved === "priority") {
|
|
1798
1826
|
params.service_tier = resolved;
|
|
1827
|
+
} else if (resolved === "ultrafast") {
|
|
1828
|
+
// Gate ultrafast to models that explicitly support it
|
|
1829
|
+
if (modelSupportsUltrafastTier(model)) {
|
|
1830
|
+
(params as Record<string, unknown>).service_tier = "ultrafast";
|
|
1831
|
+
}
|
|
1832
|
+
// else: silently omit ultrafast if model doesn't support it (fail-closed)
|
|
1799
1833
|
}
|
|
1800
1834
|
}
|
|
1801
1835
|
|
|
@@ -1988,10 +2022,28 @@ function getChoiceUsage(choice: ChatCompletionChunk.Choice): object | undefined
|
|
|
1988
2022
|
return getOptionalObjectProperty(choice, "usage");
|
|
1989
2023
|
}
|
|
1990
2024
|
|
|
2025
|
+
/**
|
|
2026
|
+
* Get the service tier cost multiplier for Completions API.
|
|
2027
|
+
* Completions API applies the same multipliers as Codex for consistency.
|
|
2028
|
+
*/
|
|
2029
|
+
function getCompletionsServiceTierCostMultiplier(serviceTier: ServiceTier | undefined): number {
|
|
2030
|
+
switch (serviceTier) {
|
|
2031
|
+
case "flex":
|
|
2032
|
+
return 0.5;
|
|
2033
|
+
case "priority":
|
|
2034
|
+
return 2;
|
|
2035
|
+
case "ultrafast":
|
|
2036
|
+
return 6;
|
|
2037
|
+
default:
|
|
2038
|
+
return 1;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
1991
2042
|
export function parseChunkUsage(
|
|
1992
2043
|
rawUsage: object,
|
|
1993
2044
|
model: Model<"openai-completions">,
|
|
1994
2045
|
premiumRequests: number | undefined,
|
|
2046
|
+
serviceTier?: ServiceTier,
|
|
1995
2047
|
): AssistantMessage["usage"] {
|
|
1996
2048
|
const promptTokenDetails = getOptionalObjectProperty(rawUsage, "prompt_tokens_details");
|
|
1997
2049
|
const completionTokenDetails = getOptionalObjectProperty(rawUsage, "completion_tokens_details");
|
|
@@ -2030,6 +2082,15 @@ export function parseChunkUsage(
|
|
|
2030
2082
|
...(premiumRequests !== undefined ? { premiumRequests } : {}),
|
|
2031
2083
|
};
|
|
2032
2084
|
calculateCost(model, usage);
|
|
2085
|
+
// Apply service tier cost multiplier (ultrafast = 6x, priority = 2x, flex = 0.5x)
|
|
2086
|
+
const multiplier = getCompletionsServiceTierCostMultiplier(serviceTier);
|
|
2087
|
+
if (multiplier !== 1) {
|
|
2088
|
+
usage.cost.input *= multiplier;
|
|
2089
|
+
usage.cost.output *= multiplier;
|
|
2090
|
+
usage.cost.cacheRead *= multiplier;
|
|
2091
|
+
usage.cost.cacheWrite *= multiplier;
|
|
2092
|
+
usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
|
|
2093
|
+
}
|
|
2033
2094
|
return usage;
|
|
2034
2095
|
}
|
|
2035
2096
|
|
|
@@ -49,7 +49,14 @@ function isReasoningEffort(value: unknown): value is NonNullable<ParsedRequest["
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function isServiceTier(value: unknown): value is NonNullable<ParsedRequest["options"]["serviceTier"]> {
|
|
52
|
-
return
|
|
52
|
+
return (
|
|
53
|
+
value === "auto" ||
|
|
54
|
+
value === "default" ||
|
|
55
|
+
value === "flex" ||
|
|
56
|
+
value === "scale" ||
|
|
57
|
+
value === "priority" ||
|
|
58
|
+
value === "ultrafast"
|
|
59
|
+
);
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
function isObj(v: unknown): v is Record<string, unknown> {
|
|
@@ -714,6 +721,7 @@ function buildResponseEnvelope(
|
|
|
714
721
|
created_at: Math.floor(message.timestamp / 1000),
|
|
715
722
|
status,
|
|
716
723
|
model: requestedModelId,
|
|
724
|
+
...(message.serviceTier ? { service_tier: message.serviceTier } : {}),
|
|
717
725
|
output: items,
|
|
718
726
|
usage,
|
|
719
727
|
...(status === "incomplete" ? { incomplete_details: { reason: "max_output_tokens" } } : {}),
|
|
@@ -1230,6 +1238,7 @@ export function encodeStream(
|
|
|
1230
1238
|
created_at: createdAt,
|
|
1231
1239
|
status,
|
|
1232
1240
|
model: requestedModelId,
|
|
1241
|
+
...(message?.serviceTier ? { service_tier: message.serviceTier } : {}),
|
|
1233
1242
|
output: items,
|
|
1234
1243
|
usage,
|
|
1235
1244
|
...(status === "incomplete" ? { incomplete_details: { reason: "max_output_tokens" } } : {}),
|
|
@@ -43,6 +43,7 @@ export declare function appendResponsesToolResultMessages<TApi extends Api>(mess
|
|
|
43
43
|
export interface ProcessResponsesStreamOptions {
|
|
44
44
|
onFirstToken?: () => void;
|
|
45
45
|
onOutputItemDone?: (item: ResponseOutputItem) => void;
|
|
46
|
+
serviceTier?: ServiceTier;
|
|
46
47
|
}
|
|
47
48
|
export declare function processResponsesStream<TApi extends Api>(openaiStream: AsyncIterable<OpenAI.Responses.ResponseStreamEvent>, output: AssistantMessage, stream: AssistantMessageEventStream, model: Model<TApi>, options?: ProcessResponsesStreamOptions): Promise<boolean>;
|
|
48
49
|
/**
|
|
@@ -99,7 +100,7 @@ type CommonSamplingOptions = Pick<StreamOptions, "temperature" | "topP" | "topK"
|
|
|
99
100
|
* Apply the common `StreamOptions` → Responses sampling-parameter mapping (max output tokens,
|
|
100
101
|
* temperature, top-p/k, min-p, presence/repetition penalties, service tier). Mutates `params`.
|
|
101
102
|
*/
|
|
102
|
-
export declare function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(params: P, options: CommonSamplingOptions | undefined, provider: string, supportsServiceTier?: boolean): void;
|
|
103
|
+
export declare function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(params: P, options: CommonSamplingOptions | undefined, provider: string, supportsServiceTier?: boolean, modelSupportsUltrafastTier?: boolean): void;
|
|
103
104
|
type ReasoningOptions = {
|
|
104
105
|
reasoning?: string;
|
|
105
106
|
reasoningSummary?: "auto" | "detailed" | "concise" | null;
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type AssistantMessage,
|
|
19
19
|
type ImageContent,
|
|
20
20
|
type Model,
|
|
21
|
+
parseResolvedServiceTier,
|
|
21
22
|
resolveServiceTier,
|
|
22
23
|
type ServiceTier,
|
|
23
24
|
type StopReason,
|
|
@@ -437,9 +438,27 @@ function appendResponsesToolResultOutput(
|
|
|
437
438
|
}
|
|
438
439
|
}
|
|
439
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Get the service tier cost multiplier for Responses API.
|
|
443
|
+
* Responses API applies the same multipliers as Codex for consistency.
|
|
444
|
+
*/
|
|
445
|
+
function getResponsesServiceTierCostMultiplier(serviceTier: ServiceTier | undefined): number {
|
|
446
|
+
switch (serviceTier) {
|
|
447
|
+
case "flex":
|
|
448
|
+
return 0.5;
|
|
449
|
+
case "priority":
|
|
450
|
+
return 2;
|
|
451
|
+
case "ultrafast":
|
|
452
|
+
return 6;
|
|
453
|
+
default:
|
|
454
|
+
return 1;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
440
458
|
export interface ProcessResponsesStreamOptions {
|
|
441
459
|
onFirstToken?: () => void;
|
|
442
460
|
onOutputItemDone?: (item: ResponseOutputItem) => void;
|
|
461
|
+
serviceTier?: ServiceTier;
|
|
443
462
|
}
|
|
444
463
|
|
|
445
464
|
export async function processResponsesStream<TApi extends Api>(
|
|
@@ -973,6 +992,21 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
973
992
|
}
|
|
974
993
|
populateResponsesUsageFromResponse(output, response?.usage);
|
|
975
994
|
calculateCost(model, output.usage);
|
|
995
|
+
// Apply service tier cost multiplier (ultrafast = 6x, priority = 2x, flex = 0.5x)
|
|
996
|
+
const effectiveServiceTier = parseResolvedServiceTier(response?.service_tier) ?? options?.serviceTier;
|
|
997
|
+
output.serviceTier = parseResolvedServiceTier(effectiveServiceTier);
|
|
998
|
+
const multiplier = getResponsesServiceTierCostMultiplier(effectiveServiceTier);
|
|
999
|
+
if (multiplier !== 1) {
|
|
1000
|
+
output.usage.cost.input *= multiplier;
|
|
1001
|
+
output.usage.cost.output *= multiplier;
|
|
1002
|
+
output.usage.cost.cacheRead *= multiplier;
|
|
1003
|
+
output.usage.cost.cacheWrite *= multiplier;
|
|
1004
|
+
output.usage.cost.total =
|
|
1005
|
+
output.usage.cost.input +
|
|
1006
|
+
output.usage.cost.output +
|
|
1007
|
+
output.usage.cost.cacheRead +
|
|
1008
|
+
output.usage.cost.cacheWrite;
|
|
1009
|
+
}
|
|
976
1010
|
// A `response.incomplete` frame is terminal because the response was cut
|
|
977
1011
|
// short, so the event type itself proves truncation. Deriving `length`
|
|
978
1012
|
// from the event rather than trusting `response.status` stops a relay that
|
|
@@ -1214,6 +1248,7 @@ export function applyCommonResponsesSamplingParams<P extends CommonResponsesPara
|
|
|
1214
1248
|
options: CommonSamplingOptions | undefined,
|
|
1215
1249
|
provider: string,
|
|
1216
1250
|
supportsServiceTier = false,
|
|
1251
|
+
modelSupportsUltrafastTier = false,
|
|
1217
1252
|
): void {
|
|
1218
1253
|
if (options?.maxTokens) params.max_output_tokens = options.maxTokens;
|
|
1219
1254
|
if (options?.temperature !== undefined) params.temperature = options.temperature;
|
|
@@ -1222,10 +1257,16 @@ export function applyCommonResponsesSamplingParams<P extends CommonResponsesPara
|
|
|
1222
1257
|
if (options?.minP !== undefined) params.min_p = options.minP;
|
|
1223
1258
|
if (options?.presencePenalty !== undefined) params.presence_penalty = options.presencePenalty;
|
|
1224
1259
|
if (options?.repetitionPenalty !== undefined) params.repetition_penalty = options.repetitionPenalty;
|
|
1225
|
-
if (shouldSendServiceTier(options?.serviceTier, provider, supportsServiceTier)) {
|
|
1260
|
+
if (shouldSendServiceTier(options?.serviceTier, provider, supportsServiceTier, modelSupportsUltrafastTier)) {
|
|
1226
1261
|
const resolved = resolveServiceTier(options?.serviceTier, provider);
|
|
1227
1262
|
if (resolved === "flex" || resolved === "scale" || resolved === "priority") {
|
|
1228
1263
|
params.service_tier = resolved;
|
|
1264
|
+
} else if (resolved === "ultrafast") {
|
|
1265
|
+
// Responses API: gate ultrafast to models that explicitly support it
|
|
1266
|
+
if (modelSupportsUltrafastTier) {
|
|
1267
|
+
(params as Record<string, unknown>).service_tier = "ultrafast";
|
|
1268
|
+
}
|
|
1269
|
+
// else: silently omit ultrafast if model doesn't support it (fail-closed)
|
|
1229
1270
|
}
|
|
1230
1271
|
}
|
|
1231
1272
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
isKnownProvider,
|
|
16
16
|
type MessageAttribution,
|
|
17
17
|
type Model,
|
|
18
|
+
modelSupportsUltrafastTier,
|
|
18
19
|
type OpenAICompat,
|
|
19
20
|
type ProviderSessionState,
|
|
20
21
|
type ServiceTier,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
resolveOpenAISdkRequestTimeoutMs,
|
|
49
50
|
} from "../utils/idle-iterator";
|
|
50
51
|
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
52
|
+
import { isLoopbackHostname } from "../utils/parse-bind";
|
|
51
53
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
52
54
|
import { callWithCopilotModelRetry } from "../utils/retry";
|
|
53
55
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
@@ -168,27 +170,49 @@ function isCanonicalOpenAIAffinityOrigin(baseUrl: string | undefined): boolean {
|
|
|
168
170
|
}
|
|
169
171
|
}
|
|
170
172
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
173
|
+
* True when the base URL is http(s) and its hostname is a loopback literal
|
|
174
|
+
* (`127.0.0.0/8`, `localhost`, `::1`). Hostnames are matched as written and
|
|
175
|
+
* never resolved, so a DNS alias pointed at loopback stays opt-in. A loopback
|
|
176
|
+
* relay runs in the same trust scope as the process that dials it, so the
|
|
177
|
+
* ChatGPT codex backend contract applies: without a stable session identity the
|
|
178
|
+
* backend cannot reuse the warmed prompt prefix, and every turn replays the
|
|
179
|
+
* whole context. Remote custom relays stay opt-in.
|
|
175
180
|
*/
|
|
181
|
+
function isLoopbackRelayOrigin(baseUrl: string | undefined): boolean {
|
|
182
|
+
if (!baseUrl) return false;
|
|
183
|
+
try {
|
|
184
|
+
const url = new URL(baseUrl);
|
|
185
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
186
|
+
return isLoopbackHostname(url.hostname);
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
176
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Official OpenAI keeps its existing session-routing behavior even when prompt
|
|
194
|
+
* caching is disabled. Relay affinity is cache-enabled and limited to explicitly
|
|
195
|
+
* supported openai or unknown provider ids so known non-target transports cannot
|
|
196
|
+
* inherit the headers. Custom relays default on for loopback origins, where the
|
|
197
|
+
* caller owns the endpoint, and an explicit `false` opts out everywhere.
|
|
198
|
+
*/
|
|
177
199
|
function shouldSendOpenAIResponsesSessionHeaders(
|
|
178
200
|
model: Model<"openai-responses">,
|
|
179
201
|
baseUrl: string | undefined,
|
|
180
202
|
cacheRetention: CacheRetention,
|
|
181
203
|
): boolean {
|
|
204
|
+
const affinityCompat = model.compat?.supportsResponsesSessionAffinity;
|
|
182
205
|
if (model.provider === "openai") {
|
|
183
206
|
if (isCanonicalOpenAIAffinityOrigin(baseUrl)) return true;
|
|
184
|
-
|
|
207
|
+
if (cacheRetention === "none" || affinityCompat === false) return false;
|
|
208
|
+
return affinityCompat === true || isLoopbackRelayOrigin(baseUrl);
|
|
185
209
|
}
|
|
186
|
-
if (cacheRetention === "none" || isKnownProvider(model.provider)) {
|
|
210
|
+
if (cacheRetention === "none" || isKnownProvider(model.provider) || affinityCompat === false) {
|
|
187
211
|
return false;
|
|
188
212
|
}
|
|
189
213
|
return (
|
|
190
214
|
Boolean(baseUrl?.trim()) &&
|
|
191
|
-
|
|
215
|
+
(affinityCompat === true || isLoopbackRelayOrigin(baseUrl)) &&
|
|
192
216
|
!isCanonicalOpenAIAffinityOrigin(baseUrl)
|
|
193
217
|
);
|
|
194
218
|
}
|
|
@@ -394,11 +418,14 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
394
418
|
options?.maxRetryDelayMs,
|
|
395
419
|
options?.attemptScope,
|
|
396
420
|
options?.streamFirstEventTimeoutMs,
|
|
421
|
+
options?.streamIdleTimeoutMs,
|
|
397
422
|
);
|
|
398
423
|
const premiumRequestsTotal = copilotPremiumRequests;
|
|
399
424
|
const providerSessionState = getOpenAIResponsesProviderSessionState(model, options?.providerSessionState);
|
|
400
425
|
let { params } = buildParams(model, context, options, providerSessionState, cacheRetention, baseUrl);
|
|
401
|
-
const idleTimeoutMs =
|
|
426
|
+
const idleTimeoutMs =
|
|
427
|
+
options?.streamIdleTimeoutMs ??
|
|
428
|
+
getOpenAIStreamIdleTimeoutMs(model.provider, model.id, model.streamIdleTimeoutMs);
|
|
402
429
|
const replacementPayload = await options?.onPayload?.(
|
|
403
430
|
params,
|
|
404
431
|
undefined,
|
|
@@ -491,6 +518,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
491
518
|
onOutputItemDone: item => {
|
|
492
519
|
nativeOutputItems.push(structuredCloneJSON<unknown>(item) as unknown as Record<string, unknown>);
|
|
493
520
|
},
|
|
521
|
+
serviceTier: (params as unknown as Record<string, unknown>).service_tier as ServiceTier | undefined,
|
|
494
522
|
},
|
|
495
523
|
);
|
|
496
524
|
if (premiumRequestsTotal !== undefined) output.usage.premiumRequests = premiumRequestsTotal;
|
|
@@ -588,6 +616,7 @@ function createClient(
|
|
|
588
616
|
maxRetryDelayMs?: number,
|
|
589
617
|
attemptScope?: import("../types.js").AttemptScopeRef,
|
|
590
618
|
streamFirstEventTimeoutOverride?: number,
|
|
619
|
+
effectiveIdleTimeoutOverride?: number,
|
|
591
620
|
): {
|
|
592
621
|
client: OpenAI;
|
|
593
622
|
copilotPremiumRequests: number | undefined;
|
|
@@ -660,7 +689,13 @@ function createClient(
|
|
|
660
689
|
);
|
|
661
690
|
// Bound HTTP request timeout to the first-event window so a stalled-before-headers
|
|
662
691
|
// fetch cannot wait the SDK's 10-minute default before the transport watchdog arms.
|
|
663
|
-
const sdkTimeoutMs = resolveOpenAISdkRequestTimeoutMs(
|
|
692
|
+
const sdkTimeoutMs = resolveOpenAISdkRequestTimeoutMs(
|
|
693
|
+
model.provider,
|
|
694
|
+
streamFirstEventTimeoutOverride,
|
|
695
|
+
model.id,
|
|
696
|
+
model.streamIdleTimeoutMs,
|
|
697
|
+
effectiveIdleTimeoutOverride,
|
|
698
|
+
);
|
|
664
699
|
return {
|
|
665
700
|
client: new OpenAI({
|
|
666
701
|
apiKey,
|
|
@@ -746,7 +781,13 @@ function buildParams(
|
|
|
746
781
|
stream_options: model.provider === "openai" ? { include_obfuscation: false } : undefined,
|
|
747
782
|
};
|
|
748
783
|
|
|
749
|
-
applyCommonResponsesSamplingParams(
|
|
784
|
+
applyCommonResponsesSamplingParams(
|
|
785
|
+
params,
|
|
786
|
+
options,
|
|
787
|
+
model.provider,
|
|
788
|
+
model.compat?.supportsServiceTier === true,
|
|
789
|
+
modelSupportsUltrafastTier(model),
|
|
790
|
+
);
|
|
750
791
|
// TODO: openai responses has no top-level `stop`/`stop_sequences`; surface via reasoning.stop?
|
|
751
792
|
// `StreamOptions.stopSequences` is intentionally dropped for this provider.
|
|
752
793
|
// TODO: openai responses has no top-level `frequency_penalty` field as of the current SDK;
|
|
@@ -326,7 +326,7 @@ function sanitizeEvent(event: AssistantMessageEvent): AssistantMessageEvent | un
|
|
|
326
326
|
case "done":
|
|
327
327
|
return { ...event, message: sanitizeMessage(event.message) };
|
|
328
328
|
case "error":
|
|
329
|
-
return {
|
|
329
|
+
return { type: "error", reason: event.reason, error: sanitizeMessage(event.error) };
|
|
330
330
|
case "toolChoiceIncapability":
|
|
331
331
|
return event;
|
|
332
332
|
case "thinking_start":
|