@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +1 -1
  3. package/dist/types/auth-broker/wire-schemas.d.ts +30 -0
  4. package/dist/types/auth-storage.d.ts +80 -8
  5. package/dist/types/bedrock-claude-cache-policy.d.ts +1 -1
  6. package/dist/types/model-cache.d.ts +6 -4
  7. package/dist/types/model-manager.d.ts +2 -0
  8. package/dist/types/openai-completions-compat.d.ts +2 -1
  9. package/dist/types/providers/anthropic.d.ts +10 -0
  10. package/dist/types/providers/aws-credential-config.d.ts +5 -0
  11. package/dist/types/providers/google-auth.d.ts +7 -0
  12. package/dist/types/providers/google-gemini-headers.d.ts +10 -5
  13. package/dist/types/providers/kiro-api-key.d.ts +5 -0
  14. package/dist/types/providers/openai-chat-server-schema.d.ts +2 -1
  15. package/dist/types/providers/openai-completions.d.ts +1 -1
  16. package/dist/types/providers/openai-responses-shared.d.ts +2 -1
  17. package/dist/types/stream.d.ts +33 -1
  18. package/dist/types/types.d.ts +26 -3
  19. package/dist/types/usage.d.ts +3 -0
  20. package/dist/types/utils/discovery/antigravity.d.ts +11 -0
  21. package/dist/types/utils/idle-iterator.d.ts +3 -2
  22. package/dist/types/utils/oauth/types.d.ts +9 -0
  23. package/package.json +4 -4
  24. package/src/auth-broker/remote-store.ts +62 -11
  25. package/src/auth-broker/wire-schemas.ts +2 -0
  26. package/src/auth-gateway/server.ts +3 -2
  27. package/src/auth-storage.ts +647 -194
  28. package/src/bedrock-claude-cache-policy.d.ts +1 -1
  29. package/src/bedrock-claude-cache-policy.ts +2 -2
  30. package/src/model-cache.ts +45 -10
  31. package/src/model-manager.ts +18 -6
  32. package/src/model-thinking.ts +37 -7
  33. package/src/models.json +2931 -1670
  34. package/src/openai-completions-compat.d.ts +2 -1
  35. package/src/openai-completions-compat.ts +5 -0
  36. package/src/provider-models/google.ts +7 -1
  37. package/src/provider-models/openai-compat.ts +26 -0
  38. package/src/providers/amazon-bedrock.ts +20 -11
  39. package/src/providers/anthropic.d.ts +10 -0
  40. package/src/providers/anthropic.ts +139 -4
  41. package/src/providers/aws-credential-config.d.ts +5 -0
  42. package/src/providers/aws-credential-config.ts +29 -1
  43. package/src/providers/google-auth.d.ts +7 -0
  44. package/src/providers/google-auth.ts +165 -4
  45. package/src/providers/google-gemini-headers.d.ts +10 -5
  46. package/src/providers/google-gemini-headers.ts +11 -6
  47. package/src/providers/google-vertex.ts +2 -4
  48. package/src/providers/kiro-api-key.d.ts +5 -0
  49. package/src/providers/kiro-api-key.ts +379 -68
  50. package/src/providers/ollama.ts +19 -9
  51. package/src/providers/openai-anthropic-shim.ts +1 -0
  52. package/src/providers/openai-chat-server-schema.ts +1 -1
  53. package/src/providers/openai-chat-server.ts +14 -2
  54. package/src/providers/openai-codex-responses.ts +21 -14
  55. package/src/providers/openai-completions.d.ts +1 -1
  56. package/src/providers/openai-completions.ts +65 -4
  57. package/src/providers/openai-responses-server.ts +10 -1
  58. package/src/providers/openai-responses-shared.d.ts +2 -1
  59. package/src/providers/openai-responses-shared.ts +42 -1
  60. package/src/providers/openai-responses.ts +51 -10
  61. package/src/providers/pi-native-server.ts +1 -1
  62. package/src/stream.d.ts +33 -1
  63. package/src/stream.ts +107 -13
  64. package/src/types.d.ts +26 -3
  65. package/src/types.ts +55 -2
  66. package/src/usage/claude.ts +4 -2
  67. package/src/usage.ts +3 -0
  68. package/src/utils/discovery/antigravity.ts +144 -2
  69. package/src/utils/idle-iterator.d.ts +3 -2
  70. package/src/utils/idle-iterator.ts +30 -10
  71. package/src/utils/oauth/anthropic.ts +23 -22
  72. package/src/utils/oauth/api-key-validation.ts +34 -11
  73. package/src/utils/oauth/types.d.ts +9 -0
  74. package/src/utils/oauth/types.ts +9 -0
  75. package/src/utils/parse-bind.d.ts +35 -0
@@ -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(content: string | Array<{ type: "text" | "image"; text?: string; data?: string }>): {
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" && typeof block.data === "string") {
165
- images.push(block.data);
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.
@@ -67,6 +67,7 @@ export function streamOpenAIAnthropicShim(
67
67
  thinking: model.thinking,
68
68
  input: model.input,
69
69
  cost: model.cost,
70
+ streamIdleTimeoutMs: model.streamIdleTimeoutMs,
70
71
  };
71
72
 
72
73
  const reasoningEffort = options?.reasoning;
@@ -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 value === "auto" || value === "default" || value === "flex" || value === "scale" || value === "priority";
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, baseChunk({}, finishReason));
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
- function getCodexServiceTierCostMultiplier(
778
- model: Pick<Model<"openai-codex-responses">, "id">,
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 model.id === "gpt-5.5" ? 2.5 : 2;
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(model, resolvedTier);
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
- applyCodexServiceTierPricing(model, output.usage, response?.service_tier, runtime.requestBodyForState.service_tier);
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 = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs(model.provider, model.id);
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(model.provider, streamFirstEventTimeoutOverride, model.id);
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 (shouldSendServiceTier(options?.serviceTier, model.provider, compat.supportsServiceTier === true)) {
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 value === "auto" || value === "default" || value === "flex" || value === "scale" || value === "priority";
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
- * Official OpenAI keeps its existing session-routing behavior even when prompt
172
- * caching is disabled. Relay affinity is opt-in, cache-enabled, and limited to
173
- * explicitly supported openai or unknown provider ids so known non-target
174
- * transports cannot inherit the headers.
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
- return cacheRetention !== "none" && model.compat?.supportsResponsesSessionAffinity === true;
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
- model.compat?.supportsResponsesSessionAffinity === true &&
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 = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs(model.provider, model.id);
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(model.provider, streamFirstEventTimeoutOverride, model.id);
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(params, options, model.provider, model.compat?.supportsServiceTier === true);
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 { ...event, error: sanitizeMessage(event.error) };
329
+ return { type: "error", reason: event.reason, error: sanitizeMessage(event.error) };
330
330
  case "toolChoiceIncapability":
331
331
  return event;
332
332
  case "thinking_start":