@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.
- package/CHANGELOG.md +22 -0
- package/README.md +3 -0
- package/dist/types/auth-broker/client.d.ts +2 -1
- package/dist/types/auth-broker/remote-store.d.ts +2 -1
- package/dist/types/auth-broker/types.d.ts +3 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +68 -0
- package/dist/types/auth-storage.d.ts +21 -1
- package/dist/types/provider-models/openai-compat.d.ts +10 -0
- package/dist/types/providers/anthropic.d.ts +9 -0
- package/dist/types/providers/register-builtins.d.ts +8 -0
- package/dist/types/providers/transform-messages.d.ts +1 -0
- package/dist/types/types.d.ts +3 -1
- package/dist/types/utils/idle-iterator.d.ts +1 -0
- package/dist/types/utils/oauth/opengateway.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/package.json +2 -2
- package/src/auth-broker/client.ts +13 -0
- package/src/auth-broker/refresher.ts +1 -0
- package/src/auth-broker/remote-store.ts +25 -0
- package/src/auth-broker/server.ts +10 -2
- package/src/auth-broker/types.ts +4 -0
- package/src/auth-broker/wire-schemas.ts +17 -1
- package/src/auth-storage.ts +202 -27
- package/src/cli.ts +1 -0
- package/src/models.json +72 -0
- package/src/provider-models/descriptors.ts +7 -0
- package/src/provider-models/openai-compat.ts +20 -0
- package/src/providers/anthropic.ts +53 -23
- package/src/providers/openai-anthropic-shim.ts +4 -0
- package/src/providers/openai-completions.ts +8 -1
- package/src/providers/openai-responses.ts +4 -1
- package/src/providers/register-builtins.ts +21 -2
- package/src/providers/transform-messages.ts +25 -6
- package/src/stream.ts +1 -0
- package/src/types.ts +7 -1
- package/src/utils/idle-iterator.ts +5 -0
- package/src/utils/oauth/index.ts +6 -0
- package/src/utils/oauth/opengateway.ts +15 -0
- package/src/utils/oauth/types.ts +1 -0
- package/src/utils/validation.ts +17 -2
- 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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
|
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;
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/utils/oauth/types.ts
CHANGED
package/src/utils/validation.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
296
|
-
}
|
|
331
|
+
sanitizedContent.push(sanitizedPart);
|
|
332
|
+
}
|
|
333
|
+
return sanitizedContent;
|
|
297
334
|
}
|
|
298
335
|
|
|
299
336
|
function sanitizeResponsesStringFieldsForReplay(item: Record<string, unknown>): void {
|