@gajae-code/ai 0.11.7 → 0.11.9

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 (41) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +3 -0
  3. package/dist/types/auth-broker/client.d.ts +2 -1
  4. package/dist/types/auth-broker/remote-store.d.ts +2 -1
  5. package/dist/types/auth-broker/types.d.ts +3 -1
  6. package/dist/types/auth-broker/wire-schemas.d.ts +68 -0
  7. package/dist/types/auth-storage.d.ts +21 -1
  8. package/dist/types/provider-models/openai-compat.d.ts +10 -0
  9. package/dist/types/providers/anthropic.d.ts +9 -0
  10. package/dist/types/providers/register-builtins.d.ts +8 -0
  11. package/dist/types/providers/transform-messages.d.ts +1 -0
  12. package/dist/types/types.d.ts +3 -1
  13. package/dist/types/utils/idle-iterator.d.ts +1 -0
  14. package/dist/types/utils/oauth/opengateway.d.ts +1 -0
  15. package/dist/types/utils/oauth/types.d.ts +1 -1
  16. package/package.json +2 -2
  17. package/src/auth-broker/client.ts +13 -0
  18. package/src/auth-broker/refresher.ts +1 -0
  19. package/src/auth-broker/remote-store.ts +25 -0
  20. package/src/auth-broker/server.ts +10 -2
  21. package/src/auth-broker/types.ts +4 -0
  22. package/src/auth-broker/wire-schemas.ts +17 -1
  23. package/src/auth-storage.ts +202 -27
  24. package/src/cli.ts +1 -0
  25. package/src/models.json +72 -0
  26. package/src/provider-models/descriptors.ts +7 -0
  27. package/src/provider-models/openai-compat.ts +20 -0
  28. package/src/providers/anthropic.ts +53 -23
  29. package/src/providers/openai-anthropic-shim.ts +4 -0
  30. package/src/providers/openai-completions.ts +8 -1
  31. package/src/providers/openai-responses.ts +4 -1
  32. package/src/providers/register-builtins.ts +21 -2
  33. package/src/providers/transform-messages.ts +25 -6
  34. package/src/stream.ts +1 -0
  35. package/src/types.ts +7 -1
  36. package/src/utils/idle-iterator.ts +5 -0
  37. package/src/utils/oauth/index.ts +6 -0
  38. package/src/utils/oauth/opengateway.ts +15 -0
  39. package/src/utils/oauth/types.ts +1 -0
  40. package/src/utils/validation.ts +17 -2
  41. package/src/utils.ts +41 -4
@@ -190,6 +190,22 @@ interface LazyStreamLimits {
190
190
  const GOOGLE_GEMINI_CLI_LAZY_STREAM_LIMITS: LazyStreamLimits = {
191
191
  defaultFirstEventTimeoutMs: 300_000,
192
192
  };
193
+ const SLOW_FIRST_EVENT_PROVIDERS = new Set(["alibaba-token-plan", "kimi-code"]);
194
+
195
+ /**
196
+ * Resolves the first-event timeout fallback for the outer lazy-stream watchdog.
197
+ * A configured wrapper-specific fallback (from `LazyStreamLimits`) always wins;
198
+ * otherwise providers known to have slow first events get a five-minute floor
199
+ * matching their inner provider-level override. Returns `undefined` for
200
+ * providers that should use the shared default.
201
+ */
202
+ export function resolveLazyStreamFirstEventFallbackMs(
203
+ provider: string,
204
+ configuredFallbackMs?: number,
205
+ ): number | undefined {
206
+ if (configuredFallbackMs !== undefined) return configuredFallbackMs;
207
+ return SLOW_FIRST_EVENT_PROVIDERS.has(provider) ? 300_000 : undefined;
208
+ }
193
209
 
194
210
  function forwardStream<TApi extends Api>(
195
211
  target: EventStreamImpl,
@@ -202,11 +218,14 @@ function forwardStream<TApi extends Api>(
202
218
  (async () => {
203
219
  try {
204
220
  const idleTimeoutMs = options.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs(limits?.defaultIdleTimeoutMs);
221
+ const firstEventFallbackMs = resolveLazyStreamFirstEventFallbackMs(
222
+ model.provider,
223
+ limits?.defaultFirstEventTimeoutMs,
224
+ );
205
225
  const watchedSource = iterateWithIdleTimeout(source, {
206
226
  idleTimeoutMs,
207
227
  firstItemTimeoutMs:
208
- options.streamFirstEventTimeoutMs ??
209
- getStreamFirstEventTimeoutMs(idleTimeoutMs, limits?.defaultFirstEventTimeoutMs),
228
+ options.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs, firstEventFallbackMs),
210
229
  errorMessage: LAZY_STREAM_IDLE_TIMEOUT_ERROR,
211
230
  firstItemErrorMessage: LAZY_STREAM_FIRST_EVENT_TIMEOUT_ERROR,
212
231
  onIdle: () => abortTracker.abortLocally(new Error(LAZY_STREAM_IDLE_TIMEOUT_ERROR)),
@@ -31,7 +31,7 @@ export function transformMessages<TApi extends Api>(
31
31
  messages: Message[],
32
32
  model: Model<TApi>,
33
33
  normalizeToolCallId?: (id: string, model: Model<TApi>, source: AssistantMessage) => string,
34
- options?: { repairLatestAssistantThinking?: boolean },
34
+ options?: { repairLatestAssistantThinking?: boolean; repairAllAssistantThinking?: boolean },
35
35
  ): Message[] {
36
36
  // Build a map of original tool call IDs to normalized IDs
37
37
  const toolCallIdMap = new Map<string, string>();
@@ -73,16 +73,29 @@ export function transformMessages<TApi extends Api>(
73
73
  // are kept so the second pass can either preserve real results or synthesize
74
74
  // an explicit aborted result without leaving dangling tool_use blocks.
75
75
  const hasPartialThinking = assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error";
76
- const dropLatestAssistantThinking =
77
- options?.repairLatestAssistantThinking === true &&
78
- index === latestAssistantIndex &&
76
+ // One-shot Anthropic replay repair. `repairLatestAssistantThinking` targets the
77
+ // "latest assistant message ... cannot be modified" 400; `repairAllAssistantThinking`
78
+ // targets the "Invalid `signature` in `thinking` block" 400, which can cite a block
79
+ // anywhere in the replayed history (e.g. after compaction/pruning rewrote an earlier
80
+ // turn), so the drop must apply to every assistant message. Within each
81
+ // message only blocks that would replay as native thinking/redacted_thinking
82
+ // are dropped; cross-model reasoning degrades to text and is preserved.
83
+ const dropAssistantThinkingForRepair =
84
+ (options?.repairAllAssistantThinking === true ||
85
+ (options?.repairLatestAssistantThinking === true && index === latestAssistantIndex)) &&
79
86
  model.api === "anthropic-messages" &&
80
87
  assistantMsg.api === "anthropic-messages";
81
88
 
82
89
  const transformedContent = assistantMsg.content.flatMap(block => {
83
90
  if (block.type === "thinking") {
84
- if (hasPartialThinking || dropLatestAssistantThinking) return [];
91
+ if (hasPartialThinking) return [];
85
92
  const sanitized = block;
93
+ // Repair must only drop blocks that would otherwise replay as native
94
+ // thinking. Cross-model/provider reasoning degrades to unsigned text
95
+ // below and was never replayed as a signed block, so it cannot be the
96
+ // signature failure — dropping it would silently lose valid context.
97
+ const replaysAsNativeThinking = mustPreserveLatestAnthropicThinking || isSameModel;
98
+ if (dropAssistantThinkingForRepair && replaysAsNativeThinking) return [];
86
99
  if (mustPreserveLatestAnthropicThinking) return sanitized;
87
100
  // For same model: keep thinking blocks with signatures (needed for replay)
88
101
  // even if the thinking text is empty (OpenAI encrypted reasoning)
@@ -97,7 +110,13 @@ export function transformMessages<TApi extends Api>(
97
110
  }
98
111
 
99
112
  if (block.type === "redactedThinking") {
100
- if (hasPartialThinking || dropLatestAssistantThinking) return [];
113
+ if (hasPartialThinking) return [];
114
+ // Same restriction as thinking blocks: cross-model/provider redacted
115
+ // blocks already drop below, so repair only needs to cover blocks that
116
+ // would replay as native redacted_thinking.
117
+ if (dropAssistantThinkingForRepair && (mustPreserveLatestAnthropicThinking || isSameModel)) {
118
+ return [];
119
+ }
101
120
  if (mustPreserveLatestAnthropicThinking) return block;
102
121
  if (isSameModel) return block;
103
122
  return [];
package/src/stream.ts CHANGED
@@ -162,6 +162,7 @@ const serviceProviderMap: Record<string, KeyResolver> = {
162
162
  "qwen-portal": () => $pickCredentialEnv("QWEN_OAUTH_TOKEN", "QWEN_PORTAL_API_KEY"),
163
163
  together: "TOGETHER_API_KEY",
164
164
  zenmux: "ZENMUX_API_KEY",
165
+ opengateway: "OPENGATEWAY_API_KEY",
165
166
  venice: "VENICE_API_KEY",
166
167
  vllm: "VLLM_API_KEY",
167
168
  xiaomi: "XIAOMI_API_KEY",
package/src/types.ts CHANGED
@@ -147,6 +147,7 @@ export type KnownProvider =
147
147
  | "minimax"
148
148
  | "opencode-go"
149
149
  | "opencode-zen"
150
+ | "opengateway"
150
151
  | "synthetic"
151
152
  | "cloudflare-ai-gateway"
152
153
  | "huggingface"
@@ -709,10 +710,15 @@ export type TSchema = ZodType | TJsonSchema;
709
710
  /** Resolve parameter types for tool execution / handlers. */
710
711
  export type Static<S> = S extends ZodType ? z.infer<S> : S extends { static: infer T } ? T : unknown;
711
712
 
713
+ export type RawArgumentRejectionCode =
714
+ | "ask-intent-review-requires-positive-round"
715
+ | "ask-intent-contract-requires-non-empty-authority"
716
+ | "ask-deep-interview-metadata-requires-deep-interview-gate";
717
+
712
718
  export type RawArgumentValidationResult =
713
719
  | { outcome: "passthrough" }
714
720
  | { outcome: "accept"; arguments: ToolCall["arguments"] }
715
- | { outcome: "reject" };
721
+ | { outcome: "reject"; code?: RawArgumentRejectionCode };
716
722
 
717
723
  export interface Tool<TParameters extends TSchema = TSchema> {
718
724
  name: string;
@@ -2,6 +2,11 @@ import { $env } from "@gajae-code/utils";
2
2
 
3
3
  const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120_000;
4
4
  const DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS = 100_000;
5
+ const KIMI_CODE_FIRST_EVENT_TIMEOUT_MS = 300_000;
6
+
7
+ export function getProviderFirstEventTimeoutFallbackMs(provider: string): number | undefined {
8
+ return provider === "kimi-code" ? KIMI_CODE_FIRST_EVENT_TIMEOUT_MS : undefined;
9
+ }
5
10
 
6
11
  function normalizeIdleTimeoutMs(value: string | undefined, fallback: number): number | undefined {
7
12
  if (value === undefined) return fallback;
@@ -240,6 +240,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
240
240
  name: "ZenMux",
241
241
  available: true,
242
242
  },
243
+ {
244
+ id: "opengateway",
245
+ name: "OpenGateway by Sionic AI",
246
+ available: true,
247
+ },
243
248
  {
244
249
  id: "vllm",
245
250
  name: "vLLM (Local OpenAI-compatible)",
@@ -386,6 +391,7 @@ export async function refreshOAuthToken(
386
391
  case "vercel-ai-gateway":
387
392
  case "qwen-portal":
388
393
  case "zenmux":
394
+ case "opengateway":
389
395
  case "vllm":
390
396
  // API keys / static bearer tokens don't expire, return as-is
391
397
  newCredentials = credentials;
@@ -0,0 +1,15 @@
1
+ /** OpenGateway (by Sionic AI) login flow (API key paste, validated via /v1/models). */
2
+ import { createApiKeyLogin } from "./api-key-login";
3
+
4
+ export const loginOpenGateway = createApiKeyLogin({
5
+ providerLabel: "OpenGateway by Sionic AI",
6
+ authUrl: "https://opengateway.ai/dashboard",
7
+ instructions: "Create or copy your OpenGateway API key",
8
+ promptMessage: "Paste your OpenGateway API key",
9
+ placeholder: "sk-...",
10
+ validation: {
11
+ kind: "models-endpoint",
12
+ provider: "OpenGateway by Sionic AI",
13
+ modelsUrl: "https://apis.opengateway.ai/v1/models",
14
+ },
15
+ });
@@ -40,6 +40,7 @@ export type OAuthProvider =
40
40
  | "openai-codex-device"
41
41
  | "opencode-go"
42
42
  | "opencode-zen"
43
+ | "opengateway"
43
44
  | "parallel"
44
45
  | "perplexity"
45
46
  | "qianfan"
@@ -25,7 +25,7 @@
25
25
  import { structuredCloneJSON } from "@gajae-code/utils";
26
26
  import type { ZodType } from "zod/v4";
27
27
  import type { $ZodIssue as ZodIssue } from "zod/v4/core";
28
- import type { Tool, ToolCall } from "../types";
28
+ import type { RawArgumentRejectionCode, Tool, ToolCall } from "../types";
29
29
  import { upgradeJsonSchemaTo202012 } from "./schema/draft";
30
30
  import {
31
31
  isJsonSchemaValueValid,
@@ -958,6 +958,15 @@ export function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall["a
958
958
  return validateToolArguments(tool, toolCall);
959
959
  }
960
960
 
961
+ const RAW_ARGUMENT_REJECTION_MESSAGES: Record<RawArgumentRejectionCode, string> = {
962
+ "ask-intent-review-requires-positive-round":
963
+ "deepInterview.intent_review is post-Round-0 only and requires a positive round",
964
+ "ask-intent-contract-requires-non-empty-authority":
965
+ "deepInterview.intent_contract requires non-empty items and confirmation_options",
966
+ "ask-deep-interview-metadata-requires-deep-interview-gate":
967
+ "deepInterview metadata cannot be combined with a non-deep-interview workflowGate",
968
+ };
969
+
961
970
  /**
962
971
  * Validates tool call arguments against the tool's schema (Zod or plain JSON
963
972
  * Schema). Applies LLM-quirk coercions (numeric strings, JSON-string
@@ -969,7 +978,13 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
969
978
  const originalArgs = toolCall.arguments;
970
979
  const rawValidation = tool.rawArgumentValidation?.(originalArgs);
971
980
  if (rawValidation?.outcome === "reject") {
972
- throw new Error(`Validation failed for tool "${toolCall.name}": raw arguments rejected before coercion`);
981
+ const base = `Validation failed for tool "${toolCall.name}": raw arguments rejected before coercion`;
982
+ const code = rawValidation.code;
983
+ const correction =
984
+ typeof code === "string" && Object.hasOwn(RAW_ARGUMENT_REJECTION_MESSAGES, code)
985
+ ? RAW_ARGUMENT_REJECTION_MESSAGES[code as RawArgumentRejectionCode]
986
+ : undefined;
987
+ throw new Error(correction ? `${base}; ${correction}` : base);
973
988
  }
974
989
  const rawArgs = rawValidation?.outcome === "accept" ? rawValidation.arguments : originalArgs;
975
990
  const ctx = getValidationContext(tool);
package/src/utils.ts CHANGED
@@ -271,17 +271,53 @@ function normalizeResponsesImageUrlForReplay(value: unknown): NormalizedResponse
271
271
  return { imageUrl: stringifyResponsesStringParamForReplay(value) };
272
272
  }
273
273
 
274
+ /**
275
+ * OpenAI Responses `input_image.image_url` must be a fetchable HTTP(S) URL or an
276
+ * image data URI. Session resident-blob materialization may leave a human-readable
277
+ * placeholder like `[Session resident imageUrl blob missing: sha256:…; …]` in this
278
+ * field; replaying that string as `image_url` makes Codex reject the entire turn
279
+ * with `invalid_value` (#2924).
280
+ */
281
+ function isProviderSafeResponsesImageUrl(value: string): boolean {
282
+ const url = value.trim();
283
+ if (url.length === 0) return false;
284
+ if (url.startsWith("https://") || url.startsWith("http://")) return true;
285
+ // Accept only image data URIs — other data: schemes are not valid image inputs.
286
+ if (url.startsWith("data:image/")) return true;
287
+ return false;
288
+ }
289
+
290
+ function hasNonEmptyResponsesFileId(part: Record<string, unknown>): boolean {
291
+ return typeof part.file_id === "string" && part.file_id.trim().length > 0;
292
+ }
293
+
274
294
  function sanitizeResponsesMessageContentForReplay(content: unknown): unknown {
275
295
  if (typeof content === "string") return neutralizeReservedControlTokens(content.toWellFormed());
276
296
  if (!Array.isArray(content)) return content;
277
- return content.map(part => {
278
- if (!part || typeof part !== "object") return part;
297
+ const sanitizedContent: unknown[] = [];
298
+ for (const part of content) {
299
+ if (!part || typeof part !== "object") {
300
+ sanitizedContent.push(part);
301
+ continue;
302
+ }
279
303
  const sanitizedPart = { ...(part as Record<string, unknown>) };
280
304
  if ("text" in sanitizedPart) {
281
305
  sanitizedPart.text = normalizeResponsesMessageTextForReplay(sanitizedPart.text);
282
306
  }
283
307
  if ("image_url" in sanitizedPart) {
284
308
  const normalizedImageUrl = normalizeResponsesImageUrlForReplay(sanitizedPart.image_url);
309
+ if (!isProviderSafeResponsesImageUrl(normalizedImageUrl.imageUrl)) {
310
+ // Keep the part when a provider file_id can stand alone; otherwise drop
311
+ // only this image part so neighboring text/history still replays.
312
+ if (!hasNonEmptyResponsesFileId(sanitizedPart)) continue;
313
+ delete sanitizedPart.image_url;
314
+ if (sanitizedPart.type === "image_url") sanitizedPart.type = "input_image";
315
+ if ("detail" in sanitizedPart && !isResponsesImageDetail(sanitizedPart.detail)) {
316
+ delete sanitizedPart.detail;
317
+ }
318
+ sanitizedContent.push(sanitizedPart);
319
+ continue;
320
+ }
285
321
  sanitizedPart.image_url = normalizedImageUrl.imageUrl;
286
322
  if (sanitizedPart.type === "image_url") {
287
323
  sanitizedPart.type = "input_image";
@@ -292,8 +328,9 @@ function sanitizeResponsesMessageContentForReplay(content: unknown): unknown {
292
328
  delete sanitizedPart.detail;
293
329
  }
294
330
  }
295
- return sanitizedPart;
296
- });
331
+ sanitizedContent.push(sanitizedPart);
332
+ }
333
+ return sanitizedContent;
297
334
  }
298
335
 
299
336
  function sanitizeResponsesStringFieldsForReplay(item: Record<string, unknown>): void {