@gajae-code/ai 0.12.21 → 0.13.0
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 +8 -1
- package/dist/types/auth-storage.d.ts +46 -4
- package/dist/types/context-cap-policy.d.ts +46 -1
- package/dist/types/providers/mock.d.ts +3 -1
- package/dist/types/providers/openai-responses.d.ts +2 -1
- package/dist/types/utils/fallback-transport.d.ts +2 -0
- package/dist/types/utils/idle-iterator.d.ts +1 -0
- package/package.json +2 -2
- package/src/auth-storage.ts +560 -91
- package/src/context-cap-policy.ts +52 -0
- package/src/providers/anthropic.ts +22 -2
- package/src/providers/mock.ts +3 -0
- package/src/providers/openai-responses.ts +34 -2
- package/src/utils/fallback-transport.ts +5 -2
- package/src/utils/idle-iterator.ts +6 -0
- package/src/utils/overflow.ts +1 -0
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import type { Api, Model } from "./types";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Codex GPT-5.6 OAuth context-window policy.
|
|
5
|
+
*
|
|
6
|
+
* Authoritative source and ownership path
|
|
7
|
+
* ---------------------------------------
|
|
8
|
+
* The authenticated Codex backend discovery endpoint (`GET {base}/codex/models`
|
|
9
|
+
* with an OAuth bearer token) is the authoritative runtime source of Codex OAuth
|
|
10
|
+
* model context-window metadata (`models[].context_window`). OpenAI owns that
|
|
11
|
+
* value; GJC consumes it read-only via `fetchCodexModels` and must not silently
|
|
12
|
+
* invent a larger limit without upstream evidence.
|
|
13
|
+
*
|
|
14
|
+
* This bundled policy is a client-side conservative guard, not a primary source.
|
|
15
|
+
* It exists because OpenAI temporarily reverted the GPT-5.6 Sol product context
|
|
16
|
+
* limit from 372K to 272K on 2026-07-13 (staff announcement quoted in
|
|
17
|
+
* gajae-code issues #2240 / #2260), while planning to restore 372K later.
|
|
18
|
+
* `ceiling` must only be raised with an upstream evidence citation; an
|
|
19
|
+
* unverified report that "372K is live again" is not sufficient.
|
|
20
|
+
*
|
|
21
|
+
* Precedence (highest -> lowest)
|
|
22
|
+
* ------------------------------
|
|
23
|
+
* 1. Explicit user per-model override (`contextWindow` in `modelOverrides`).
|
|
24
|
+
* The model registry merges it into `model.contextWindow` before this cap and
|
|
25
|
+
* passes it here; it is honored when a positive finite number, with
|
|
26
|
+
* diagnostics emitted at the registry. Never silently discarded.
|
|
27
|
+
* 2. Live OAuth discovery metadata (`context_window`), forced to the enforced
|
|
28
|
+
* product window for the GPT-5.6 tier.
|
|
29
|
+
* 3. Bundled conservative generic window (`CODEX_GENERIC_CONTEXT_WINDOW`),
|
|
30
|
+
* used when discovery metadata is absent or invalid.
|
|
31
|
+
*/
|
|
3
32
|
export interface CodexGpt56ContextCapPolicy {
|
|
4
33
|
/**
|
|
5
34
|
* Usable prompt budget forced for the GPT-5.6 tier on the Codex product
|
|
@@ -37,6 +66,9 @@ export function isCodexProductTransport(model: Pick<Model<Api>, "api" | "provide
|
|
|
37
66
|
export function isCodexGpt56Tier(model: Pick<Model<Api>, "id">): boolean {
|
|
38
67
|
return CODEX_GPT_5_6_MODEL_IDS.has(model.id.toLowerCase());
|
|
39
68
|
}
|
|
69
|
+
export function codexContextOverrideKey(provider: string, modelId: string): string {
|
|
70
|
+
return `${provider.toLowerCase()}:${modelId.toLowerCase()}`;
|
|
71
|
+
}
|
|
40
72
|
|
|
41
73
|
export function resolveCodexGpt56DiscoveryContext(
|
|
42
74
|
model: Pick<Model<Api>, "api" | "id" | "provider">,
|
|
@@ -54,14 +86,34 @@ export function resolveCodexGpt56DiscoveryContext(
|
|
|
54
86
|
return policy.enforced;
|
|
55
87
|
}
|
|
56
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Applies the final Codex GPT-5.6 context ceiling, honoring explicit user
|
|
91
|
+
* overrides.
|
|
92
|
+
*
|
|
93
|
+
* `userContextWindowOverrides` maps provider-qualified composite keys
|
|
94
|
+
* (`provider:modelId`, both lowercased, built by
|
|
95
|
+
* {@link codexContextOverrideKey}) to the user's explicit `contextWindow` value
|
|
96
|
+
* (already merged into `model.contextWindow` by the model registry). A tier
|
|
97
|
+
* model present with a positive finite value keeps its value even above
|
|
98
|
+
* `enforced` — the user's explicit, diagnosed choice. Every other tier
|
|
99
|
+
* model is forced to `enforced`, so a stale larger live/cached observation
|
|
100
|
+
* (e.g. a pre-rollback 373K cache) cannot resurface without an override.
|
|
101
|
+
* Because the key is provider-qualified, an override only exempts the exact
|
|
102
|
+
* provider+model pair it was configured for.
|
|
103
|
+
*/
|
|
57
104
|
export function applyFinalCodexGpt56ContextCap<TApi extends Api>(
|
|
58
105
|
models: readonly Model<TApi>[],
|
|
59
106
|
policy: CodexGpt56ContextCapPolicy = CODEX_GPT_5_6_CONTEXT_CAP,
|
|
107
|
+
userContextWindowOverrides: ReadonlyMap<string, number> = new Map(),
|
|
60
108
|
): Model<TApi>[] {
|
|
61
109
|
return models.map(model => {
|
|
62
110
|
if (!isCodexGpt56Tier(model as Model<Api>) || !isCodexProductTransport(model as Model<Api>)) {
|
|
63
111
|
return model;
|
|
64
112
|
}
|
|
113
|
+
const userOverride = userContextWindowOverrides.get(codexContextOverrideKey(model.provider, model.id));
|
|
114
|
+
if (userOverride !== undefined && isPositiveFiniteNumber(userOverride)) {
|
|
115
|
+
return model;
|
|
116
|
+
}
|
|
65
117
|
return { ...model, contextWindow: policy.enforced };
|
|
66
118
|
});
|
|
67
119
|
}
|
|
@@ -68,6 +68,7 @@ import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } fr
|
|
|
68
68
|
import {
|
|
69
69
|
FirstEventTimeoutError,
|
|
70
70
|
getProviderFirstEventTimeoutFallbackMs,
|
|
71
|
+
getProviderStreamIdleTimeoutFallbackMs,
|
|
71
72
|
getStreamFirstEventTimeoutMs,
|
|
72
73
|
getStreamIdleTimeoutMs,
|
|
73
74
|
iterateWithIdleTimeout,
|
|
@@ -1479,6 +1480,10 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1479
1480
|
let thinkingReplayRepairScope: AnthropicThinkingReplayRepairScope =
|
|
1480
1481
|
providerSessionState?.thinkingReplayRepairScope ?? "none";
|
|
1481
1482
|
let thinkingReplayRepairAttempts = providerSessionState?.thinkingReplayRepairAttempts ?? 0;
|
|
1483
|
+
// A scope inherited from an earlier turn can only have come from the
|
|
1484
|
+
// deterministic branch below — the speculative masked-`api_error` probe is
|
|
1485
|
+
// never persisted — so a completed stream must not release it.
|
|
1486
|
+
let thinkingReplayRepairPersistent = thinkingReplayRepairScope !== "none";
|
|
1482
1487
|
let generatedCacheBudget: GeneratedCacheBudget = providerSessionState?.generatedCacheBudget ?? 2;
|
|
1483
1488
|
const prepareParams = async (): Promise<MessageCreateParamsStreaming> => {
|
|
1484
1489
|
// Degradation state is cumulative: every fallback rebuild must merge all
|
|
@@ -1579,7 +1584,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1579
1584
|
truncatedToolCalls.clear();
|
|
1580
1585
|
sawTerminalStopReason = false;
|
|
1581
1586
|
};
|
|
1582
|
-
const idleTimeoutMs =
|
|
1587
|
+
const idleTimeoutMs =
|
|
1588
|
+
options?.streamIdleTimeoutMs ??
|
|
1589
|
+
getStreamIdleTimeoutMs(getProviderStreamIdleTimeoutFallbackMs(model.provider));
|
|
1583
1590
|
const firstEventFallbackMs = getProviderFirstEventTimeoutFallbackMs(model.provider);
|
|
1584
1591
|
const firstEventTimeoutMs =
|
|
1585
1592
|
options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs, firstEventFallbackMs);
|
|
@@ -1915,7 +1922,17 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1915
1922
|
// branch above fires on an error nobody can classify, so keeping its
|
|
1916
1923
|
// guess would silently strip native thinking replay from every later
|
|
1917
1924
|
// turn of the session over what may have been one transient blip.
|
|
1918
|
-
|
|
1925
|
+
//
|
|
1926
|
+
// A deterministic rejection ("cannot be modified" / invalid signature) is
|
|
1927
|
+
// the opposite case: it cites blocks that stay in this session's history,
|
|
1928
|
+
// so releasing the repair here makes the next turn replay the same blocks
|
|
1929
|
+
// and spend another rejected round trip on every turn that follows. That
|
|
1930
|
+
// repair has to outlive the stream it fixed.
|
|
1931
|
+
if (
|
|
1932
|
+
providerSessionState &&
|
|
1933
|
+
!thinkingReplayRepairPersistent &&
|
|
1934
|
+
(thinkingReplayRepairScope !== "none" || thinkingReplayRepairAttempts > 0)
|
|
1935
|
+
) {
|
|
1919
1936
|
providerSessionState.thinkingReplayRepairScope = "none";
|
|
1920
1937
|
providerSessionState.thinkingReplayRepairAttempts = 0;
|
|
1921
1938
|
}
|
|
@@ -2004,6 +2021,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
2004
2021
|
error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
|
|
2005
2022
|
});
|
|
2006
2023
|
thinkingReplayRepairScope = nextScope;
|
|
2024
|
+
// Anything but the masked probe is caused by blocks that remain in
|
|
2025
|
+
// history, so this repair must survive the stream it is about to fix.
|
|
2026
|
+
if (!maskedProxyRejection) thinkingReplayRepairPersistent = true;
|
|
2007
2027
|
if (providerSessionState) {
|
|
2008
2028
|
providerSessionState.thinkingReplayRepairAttempts = thinkingReplayRepairAttempts;
|
|
2009
2029
|
if (!maskedProxyRejection) {
|
package/src/providers/mock.ts
CHANGED
|
@@ -87,6 +87,8 @@ export interface MockResponse {
|
|
|
87
87
|
usage?: Partial<Omit<Usage, "cost">> & { cost?: Partial<Usage["cost"]> };
|
|
88
88
|
/** Pre-set responseId. */
|
|
89
89
|
responseId?: string;
|
|
90
|
+
/** Optional typed provider failure metadata for retry/fallback tests. */
|
|
91
|
+
transportFailure?: AssistantMessage["transportFailure"];
|
|
90
92
|
/** If set, the stream emits a terminal error event instead of completing. */
|
|
91
93
|
throw?: string | Error;
|
|
92
94
|
/** Delay before any event is emitted. Honors the call's AbortSignal. */
|
|
@@ -363,6 +365,7 @@ async function runMock(
|
|
|
363
365
|
provider: model.provider,
|
|
364
366
|
model: model.id,
|
|
365
367
|
responseId: response.responseId,
|
|
368
|
+
transportFailure: response.transportFailure,
|
|
366
369
|
usage: emptyUsage(),
|
|
367
370
|
stopReason: "stop",
|
|
368
371
|
timestamp: startedAt,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
} from "../utils";
|
|
37
37
|
import { createAbortSourceTracker } from "../utils/abort";
|
|
38
38
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
39
|
-
import { transportFailureFacts } from "../utils/fallback-transport";
|
|
39
|
+
import { EMPTY_RESPONSE_PROVIDER_CODE, transportFailureFacts } from "../utils/fallback-transport";
|
|
40
40
|
import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
|
|
41
41
|
import {
|
|
42
42
|
FirstEventTimeoutError,
|
|
@@ -320,6 +320,26 @@ type OpenAIResponsesSamplingParams = ResponseCreateParamsStreaming & {
|
|
|
320
320
|
stream_options?: { include_obfuscation?: boolean };
|
|
321
321
|
};
|
|
322
322
|
|
|
323
|
+
export function isOpenCodeGoEmptyCompletedResponse(
|
|
324
|
+
model: Model<"openai-responses">,
|
|
325
|
+
output: AssistantMessage,
|
|
326
|
+
nativeOutputItemCount: number,
|
|
327
|
+
): boolean {
|
|
328
|
+
return (
|
|
329
|
+
model.provider === "opencode-go" &&
|
|
330
|
+
typeof output.responseId === "string" &&
|
|
331
|
+
output.responseId.length > 0 &&
|
|
332
|
+
output.stopReason === "stop" &&
|
|
333
|
+
output.content.length === 0 &&
|
|
334
|
+
nativeOutputItemCount === 0 &&
|
|
335
|
+
output.usage.input === 0 &&
|
|
336
|
+
output.usage.output === 0 &&
|
|
337
|
+
output.usage.cacheRead === 0 &&
|
|
338
|
+
output.usage.cacheWrite === 0 &&
|
|
339
|
+
output.usage.totalTokens === 0
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
323
343
|
/**
|
|
324
344
|
* Generate function for OpenAI Responses API
|
|
325
345
|
*/
|
|
@@ -462,11 +482,23 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
462
482
|
}
|
|
463
483
|
|
|
464
484
|
output.providerPayload = createOpenAIResponsesHistoryPayload(model.provider, nativeOutputItems);
|
|
485
|
+
if (isOpenCodeGoEmptyCompletedResponse(model, output, nativeOutputItems.length)) {
|
|
486
|
+
output.stopReason = "error";
|
|
487
|
+
output.errorMessage = "Provider returned an empty response with zero token usage";
|
|
488
|
+
output.transportFailure = {
|
|
489
|
+
kind: "transport",
|
|
490
|
+
providerCode: EMPTY_RESPONSE_PROVIDER_CODE,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
465
493
|
if (providerSessionState) providerSessionState.nativeHistoryReplayWarmed = true;
|
|
466
494
|
|
|
467
495
|
output.duration = Date.now() - startTime;
|
|
468
496
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
469
|
-
|
|
497
|
+
if (output.stopReason === "error") {
|
|
498
|
+
stream.push({ type: "error", reason: "error", error: output });
|
|
499
|
+
} else {
|
|
500
|
+
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
501
|
+
}
|
|
470
502
|
stream.end();
|
|
471
503
|
} catch (error) {
|
|
472
504
|
for (const block of output.content) delete (block as { index?: number }).index;
|
|
@@ -24,6 +24,8 @@ export interface FallbackTrigger {
|
|
|
24
24
|
|
|
25
25
|
/** Stable code for streams that time out before producing semantic progress. */
|
|
26
26
|
export const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
|
|
27
|
+
/** Stable code for a nominally successful response with no content or token usage. */
|
|
28
|
+
export const EMPTY_RESPONSE_PROVIDER_CODE = "empty_response";
|
|
27
29
|
|
|
28
30
|
export type TransportHeaders = Headers | Record<string, string | undefined>;
|
|
29
31
|
|
|
@@ -212,7 +214,8 @@ export function transportFailureFacts(
|
|
|
212
214
|
!isAuthCode(normalizedCode) &&
|
|
213
215
|
!isRateLimitCode(normalizedCode) &&
|
|
214
216
|
!isContextOverflowCode(normalizedCode) &&
|
|
215
|
-
normalizedCode !== STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE
|
|
217
|
+
normalizedCode !== STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE &&
|
|
218
|
+
normalizedCode !== EMPTY_RESPONSE_PROVIDER_CODE
|
|
216
219
|
) {
|
|
217
220
|
return undefined;
|
|
218
221
|
}
|
|
@@ -320,7 +323,7 @@ export function classifyFallbackTrigger(
|
|
|
320
323
|
);
|
|
321
324
|
const code = codes[0] ?? codes[1] ?? codes[2];
|
|
322
325
|
const triggerClass: FallbackTriggerClass =
|
|
323
|
-
code === STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE
|
|
326
|
+
code === STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE || code === EMPTY_RESPONSE_PROVIDER_CODE
|
|
324
327
|
? "server"
|
|
325
328
|
: isQuotaCode(code)
|
|
326
329
|
? "quota"
|
|
@@ -6,6 +6,12 @@ const DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS = 100_000;
|
|
|
6
6
|
const ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MS = 600_000;
|
|
7
7
|
const KIMI_CODE_FIRST_EVENT_TIMEOUT_MS = 300_000;
|
|
8
8
|
|
|
9
|
+
const ANTHROPIC_STREAM_IDLE_TIMEOUT_MS = 300_000;
|
|
10
|
+
|
|
11
|
+
export function getProviderStreamIdleTimeoutFallbackMs(provider: string): number | undefined {
|
|
12
|
+
return provider === "anthropic" ? ANTHROPIC_STREAM_IDLE_TIMEOUT_MS : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
export function getProviderFirstEventTimeoutFallbackMs(provider: string): number | undefined {
|
|
10
16
|
if (provider === "alibaba-token-plan") return ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MS;
|
|
11
17
|
return provider === "kimi-code" ? KIMI_CODE_FIRST_EVENT_TIMEOUT_MS : undefined;
|
package/src/utils/overflow.ts
CHANGED