@bitkyc08/opencodex 2.89.0 → 2.91.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/gui/dist/assets/App-DMps4OLk.css +1 -0
- package/gui/dist/assets/App-nZ34cR5G.js +58 -0
- package/gui/dist/assets/{Tray-7z1COnmz.js → Tray-BuLy7hL1.js} +1 -1
- package/gui/dist/assets/{index-uchD-ngf.js → index-DGXS51sp.js} +15 -15
- package/gui/dist/assets/quota-summary-DO1Sx5TM.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic.ts +2 -5
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +28 -2
- package/src/adapters/coding-agent/turn.ts +14 -4
- package/src/adapters/cursor/protobuf-request.ts +3 -0
- package/src/adapters/google-progress-history.ts +19 -1
- package/src/adapters/google.ts +28 -9
- package/src/adapters/image.ts +5 -3
- package/src/adapters/input-media-guard.ts +5 -2
- package/src/adapters/kiro-refusal.ts +41 -1
- package/src/adapters/ollama-native.ts +1 -0
- package/src/adapters/openai-chat/messages.ts +34 -4
- package/src/adapters/openai-chat/passthrough.ts +37 -4
- package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
- package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
- package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
- package/src/adapters/openai-chat.ts +7 -6
- package/src/adapters/openai-responses/opengateway-input.ts +139 -0
- package/src/adapters/openai-responses/passthrough.ts +7 -2
- package/src/adapters/openai-responses/request-strips.ts +32 -0
- package/src/adapters/openai-responses/transport.ts +19 -1
- package/src/chat/inbound.ts +20 -2
- package/src/chat/outbound.ts +36 -4
- package/src/chat/reasoning-details.ts +137 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +20 -10
- package/src/claude/desktop-picker-profile.ts +18 -9
- package/src/claude/desktop-remote-store-io.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +50 -0
- package/src/cli/capabilities-base.ts +15 -2
- package/src/cli/capabilities-observe-system.ts +4 -4
- package/src/cli/codex-shim-autorestore.ts +5 -0
- package/src/cli/dispatch.ts +30 -0
- package/src/cli/ensure-desktop-cold-start.ts +2 -2
- package/src/cli/fallback.ts +127 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +1 -2
- package/src/cli/link-apply-restart.ts +72 -0
- package/src/cli/link-child.ts +6 -2
- package/src/cli/link-cleanup.ts +95 -0
- package/src/cli/link-enrollment.ts +4 -0
- package/src/cli/link.ts +84 -9
- package/src/cli/provider.ts +44 -2
- package/src/cli/registry.ts +13 -1
- package/src/cli/restart-failure.ts +4 -0
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/status-codex-settings.ts +56 -0
- package/src/cli/status-fallback.ts +29 -0
- package/src/cli/status-install.ts +52 -0
- package/src/cli/status.ts +20 -1
- package/src/cli/system-restart-client.ts +32 -1
- package/src/cli/update-restart-child.ts +4 -3
- package/src/cli/update-restart-home.ts +20 -8
- package/src/client/connect.ts +82 -6
- package/src/client/home-unreachable.ts +16 -0
- package/src/client/hub-relay.ts +8 -2
- package/src/client/link-home.ts +119 -6
- package/src/client/link-parity-reporter.ts +107 -0
- package/src/client/link-relay-transport.ts +8 -1
- package/src/client/link-relay.ts +27 -3
- package/src/client/machine-listener.ts +83 -1
- package/src/client/runtime-restart.ts +127 -0
- package/src/client/runtime.ts +137 -25
- package/src/codex/account-lifecycle.ts +19 -6
- package/src/codex/account-store.ts +27 -8
- package/src/codex/account-usability.ts +4 -1
- package/src/codex/auth-api/account-list.ts +10 -5
- package/src/codex/auth-api/account-pause-group.ts +2 -22
- package/src/codex/auth-api/login-collision.ts +59 -0
- package/src/codex/auth-api/login-flow.ts +12 -7
- package/src/codex/auth-api/pool-mode-gate.ts +81 -13
- package/src/codex/auth-collision.ts +31 -4
- package/src/codex/auth-context.ts +58 -12
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
- package/src/codex/main-account-hard-lock.ts +14 -0
- package/src/codex/quota-history.ts +7 -0
- package/src/codex/quota.ts +10 -0
- package/src/codex/routing/cooldown-math.ts +5 -0
- package/src/codex/routing/failure-window.ts +61 -9
- package/src/codex/routing.ts +4 -3
- package/src/codex/shim-diagnostics.ts +2 -1
- package/src/codex/shim-fingerprint.ts +11 -2
- package/src/codex/shim-templates.ts +16 -5
- package/src/codex/shim-windows-refresh.ts +224 -0
- package/src/codex/shim.ts +45 -21
- package/src/codex/subagent-model-fallback.ts +24 -1
- package/src/combos/failover.ts +8 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/diagnostics.ts +5 -0
- package/src/config/load-degrade.ts +2 -0
- package/src/config/provider-validation.ts +7 -2
- package/src/config/schema/config-schema.ts +3 -0
- package/src/config/schema/fallback-policy.ts +40 -0
- package/src/generated/compatibility-version.json +360 -192
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +2 -2
- package/src/lib/authorization-scheme.ts +95 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +31 -17
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/provider-client-headers.ts +1 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/system-restart-contract.ts +24 -0
- package/src/lib/test-home-guard.ts +78 -11
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -0
- package/src/link/child-parity.ts +124 -0
- package/src/link/codex-settings.ts +37 -0
- package/src/link/compatibility.ts +119 -0
- package/src/link/management-routes.ts +2 -0
- package/src/link/peer-report.ts +100 -0
- package/src/link/remote-platform.ts +78 -0
- package/src/link/ssh-argv.ts +19 -0
- package/src/link/status-projection.ts +24 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
- package/src/oauth/account-import/parser.ts +15 -2
- package/src/oauth/account-import/types.ts +7 -0
- package/src/oauth/chatgpt-shared.ts +10 -1
- package/src/oauth/dsh.ts +45 -2
- package/src/oauth/index.ts +1 -1
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/oauth/nous.ts +4 -1
- package/src/oauth/store.ts +2 -1
- package/src/protocols/codecs/chat.ts +3 -3
- package/src/protocols/encoders/adapter-events.ts +26 -2
- package/src/protocols/encoders/chat.ts +55 -3
- package/src/providers/claude-cli-usage.ts +20 -5
- package/src/providers/initial-model-selection.ts +2 -2
- package/src/providers/key-failover.ts +10 -2
- package/src/providers/key-store.ts +8 -3
- package/src/providers/mistral-transport.ts +240 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota/anthropic-account-quota.ts +18 -6
- package/src/providers/quota/antigravity.ts +8 -10
- package/src/providers/quota/failure-codes.ts +18 -0
- package/src/providers/quota/vendor-probes-oauth.ts +12 -3
- package/src/providers/quota-routing-cache.ts +1 -1
- package/src/providers/quota.ts +5 -1
- package/src/providers/registry/entries-extended.ts +6 -1
- package/src/providers/routed-config.ts +1 -1
- package/src/providers/xai-transport.ts +2 -1
- package/src/quota-pool/assemble.ts +326 -0
- package/src/quota-pool/contract.ts +58 -0
- package/src/quota-pool/identity.ts +203 -0
- package/src/remote/features.ts +74 -0
- package/src/remote/protocol.ts +2 -0
- package/src/remote/served-by.ts +57 -0
- package/src/responses/audio.ts +46 -0
- package/src/responses/input-media.ts +37 -15
- package/src/responses/parser-content.ts +22 -27
- package/src/responses/parser.ts +7 -0
- package/src/responses/reasoning-envelope.ts +12 -0
- package/src/responses/reasoning-retention.ts +218 -11
- package/src/responses/schema.ts +3 -4
- package/src/responses/web-search-replay.ts +11 -2
- package/src/routing/fallback-policy.ts +290 -0
- package/src/routing/fallback-state.ts +179 -0
- package/src/server/audio-client.ts +2 -1
- package/src/server/audio-dictation.ts +2 -1
- package/src/server/audio-upstream.ts +2 -1
- package/src/server/auth-cors.ts +6 -5
- package/src/server/background-lifecycle.ts +4 -0
- package/src/server/chat-completions.ts +8 -1
- package/src/server/chat-native.ts +8 -7
- package/src/server/gui-session.ts +2 -1
- package/src/server/index/link-listener.ts +11 -1
- package/src/server/index/link-management.ts +47 -0
- package/src/server/index/link-relay-sessions.ts +11 -2
- package/src/server/index/optional-listeners.ts +7 -2
- package/src/server/index/serve-options.ts +27 -4
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/live.ts +5 -0
- package/src/server/management/context.ts +1 -1
- package/src/server/management/fallback-routes.ts +75 -0
- package/src/server/management/link-enrollment.ts +21 -3
- package/src/server/management/link-route-state.ts +2 -0
- package/src/server/management/link-routes.ts +12 -1
- package/src/server/management/oauth-account-routes.ts +2 -1
- package/src/server/management/quota-pool-routes.ts +24 -0
- package/src/server/management/route-registry.ts +3 -0
- package/src/server/management-api.ts +16 -0
- package/src/server/management-auth.ts +2 -1
- package/src/server/messages-native.ts +11 -0
- package/src/server/request-log.ts +16 -7
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +2 -2
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/codex-ws-exchange.ts +3 -0
- package/src/server/responses/codex-ws-metadata.ts +44 -0
- package/src/server/responses/codex-ws-session.ts +21 -1
- package/src/server/responses/combo-stream-preflight.ts +7 -2
- package/src/server/responses/compact.ts +4 -2
- package/src/server/responses/core-auth.ts +31 -1
- package/src/server/responses/core-codex-account.ts +21 -14
- package/src/server/responses/core-combo.ts +75 -1
- package/src/server/responses/core-opaque-recovery.ts +20 -11
- package/src/server/responses/deepseek-image-files.ts +121 -0
- package/src/server/responses/empty-completion-guard.ts +8 -0
- package/src/server/responses/encrypted-payload.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +30 -10
- package/src/server/responses/input-admission.ts +6 -2
- package/src/server/responses/main-hard-lock-routing.ts +85 -8
- package/src/server/responses/opengateway-completion-guard.ts +232 -0
- package/src/server/responses/passthrough-completion.ts +41 -0
- package/src/server/responses/passthrough-delivery.ts +2 -0
- package/src/server/responses/passthrough-dispatch.ts +13 -13
- package/src/server/responses/passthrough-key-rotation.ts +40 -0
- package/src/server/responses/policy-fallback.ts +44 -1
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-prepare.ts +19 -1
- package/src/server/responses/request-transport.ts +61 -3
- package/src/server/restart-replacement.ts +50 -2
- package/src/server/ws-bridge.ts +5 -0
- package/src/service/cli.ts +25 -2
- package/src/service/diagnostics.ts +20 -6
- package/src/service/orchestration.ts +17 -0
- package/src/service/status-json.ts +116 -0
- package/src/types/config.ts +7 -0
- package/src/types/fallback.ts +38 -0
- package/src/types/provider.ts +2 -0
- package/src/types/request.ts +16 -1
- package/src/types.ts +1 -0
- package/src/web-search/loop.ts +2 -2
- package/gui/dist/assets/App-BnRSujOq.js +0 -58
- package/gui/dist/assets/App-dnkJ85te.css +0 -1
- package/gui/dist/assets/quota-summary-BY_10IEz.js +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { validateAntigravityImportCredential } from "../google-antigravity";
|
|
2
2
|
import { upsertCredentialByIdentity } from "../store";
|
|
3
3
|
import type { OAuthCredentials } from "../types";
|
|
4
|
+
import { probeAntigravityUsageQuota } from "../../providers/quota/antigravity";
|
|
4
5
|
import {
|
|
5
6
|
ACCOUNT_IMPORT_FORMAT,
|
|
6
7
|
ACCOUNT_IMPORT_MAX_EMAIL_LENGTH,
|
|
@@ -14,6 +15,26 @@ import {
|
|
|
14
15
|
export interface AntigravityImportAdapterDeps {
|
|
15
16
|
validate(refreshToken: string, signal?: AbortSignal): Promise<ValidatedAntigravityCredential>;
|
|
16
17
|
upsert(credential: OAuthCredentials): Promise<"inserted" | "updated">;
|
|
18
|
+
/**
|
|
19
|
+
* Prove that the freshly validated account token can use an operator-supplied project.
|
|
20
|
+
* Without this owner a fallback project is never admitted.
|
|
21
|
+
*/
|
|
22
|
+
verifyProject?(accessToken: string, projectId: string, signal?: AbortSignal): Promise<boolean>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The account-bound admission probe for a fallback project: the same Cloud Code Assist quota
|
|
27
|
+
* probe the account pool runs, authenticated with this account's own access token against the
|
|
28
|
+
* named project. Only a parsed quota answer admits it; denial, rate limiting, transport failure
|
|
29
|
+
* or an unusable body all keep the record out.
|
|
30
|
+
*/
|
|
31
|
+
export async function verifyAntigravityImportProject(accessToken: string, projectId: string, signal?: AbortSignal): Promise<boolean> {
|
|
32
|
+
if (signal?.aborted) return false;
|
|
33
|
+
try {
|
|
34
|
+
return (await probeAntigravityUsageQuota(accessToken, projectId, signal)).kind === "available" && !signal?.aborted;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
17
38
|
}
|
|
18
39
|
|
|
19
40
|
function safeProviderEmail(value: unknown): string | null {
|
|
@@ -32,6 +53,7 @@ export function createAntigravityAccountImportAdapter(
|
|
|
32
53
|
deps: AntigravityImportAdapterDeps = {
|
|
33
54
|
validate: validateAntigravityImportCredential,
|
|
34
55
|
upsert: credential => upsertCredentialByIdentity(ACCOUNT_IMPORT_PROVIDER, credential),
|
|
56
|
+
verifyProject: verifyAntigravityImportProject,
|
|
35
57
|
},
|
|
36
58
|
): AccountImportAdapter {
|
|
37
59
|
return {
|
|
@@ -52,15 +74,29 @@ export function createAntigravityAccountImportAdapter(
|
|
|
52
74
|
const providerEmail = safeProviderEmail(credential.email);
|
|
53
75
|
if (!providerEmail) return { status: "failed", code: "credential_rejected" };
|
|
54
76
|
if (providerEmail !== record.email) return { status: "failed", code: "identity_mismatch" };
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
// Automatic discovery wins: a discovered project is used as-is and the record's project_id
|
|
78
|
+
// is ignored. The record's project is a fallback only, admitted after an account-bound probe.
|
|
79
|
+
let projectId = typeof credential.projectId === "string" && credential.projectId.length > 0
|
|
80
|
+
? credential.projectId
|
|
81
|
+
: undefined;
|
|
82
|
+
if (!projectId && record.projectId && deps.verifyProject) {
|
|
83
|
+
let admitted = false;
|
|
84
|
+
try {
|
|
85
|
+
admitted = await deps.verifyProject(credential.access, record.projectId, signal);
|
|
86
|
+
} catch {
|
|
87
|
+
admitted = false;
|
|
88
|
+
}
|
|
89
|
+
throwIfAccountImportAborted(signal);
|
|
90
|
+
if (admitted) projectId = record.projectId;
|
|
57
91
|
}
|
|
92
|
+
if (!projectId) return { status: "failed", code: "missing_project" };
|
|
58
93
|
|
|
59
94
|
throwIfAccountImportAborted(signal);
|
|
60
95
|
try {
|
|
61
96
|
const disposition = await deps.upsert({
|
|
62
97
|
...credential,
|
|
63
98
|
email: providerEmail,
|
|
99
|
+
projectId,
|
|
64
100
|
source: "credential-file",
|
|
65
101
|
});
|
|
66
102
|
return disposition === "updated"
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ACCOUNT_IMPORT_MAX_BYTES,
|
|
3
3
|
ACCOUNT_IMPORT_MAX_EMAIL_LENGTH,
|
|
4
|
+
ACCOUNT_IMPORT_MAX_PROJECT_ID_LENGTH,
|
|
4
5
|
ACCOUNT_IMPORT_MAX_RECORDS,
|
|
5
6
|
ACCOUNT_IMPORT_MAX_REFRESH_TOKEN_BYTES,
|
|
6
7
|
type ParsedCockpitRecord,
|
|
7
8
|
} from "./types";
|
|
8
9
|
|
|
9
10
|
const encoder = new TextEncoder();
|
|
10
|
-
const ALLOWED_RECORD_KEYS = new Set(["email", "refresh_token", "tags", "notes"]);
|
|
11
|
+
const ALLOWED_RECORD_KEYS = new Set(["email", "refresh_token", "project_id", "tags", "notes"]);
|
|
12
|
+
// Google Cloud project id grammar: a lowercase letter, then lowercase letters, digits or hyphens,
|
|
13
|
+
// not ending in a hyphen. Exact match only — no trimming, so whitespace or controls are rejected.
|
|
14
|
+
const PROJECT_ID_SHAPE = /^[a-z][a-z0-9-]*[a-z0-9]$/;
|
|
11
15
|
|
|
12
16
|
export type CockpitDocumentParseResult =
|
|
13
17
|
| { ok: true; records: ParsedCockpitRecord[] }
|
|
@@ -50,10 +54,18 @@ function validMetadata(record: Record<string, unknown>): boolean {
|
|
|
50
54
|
return record.notes === undefined || typeof record.notes === "string";
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
function validProjectId(value: unknown): value is string | undefined {
|
|
58
|
+
if (value === undefined) return true;
|
|
59
|
+
return typeof value === "string"
|
|
60
|
+
&& value.length >= 6
|
|
61
|
+
&& value.length <= ACCOUNT_IMPORT_MAX_PROJECT_ID_LENGTH
|
|
62
|
+
&& PROJECT_ID_SHAPE.test(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
function parseRecord(value: unknown, index: number): ParsedCockpitRecord {
|
|
54
66
|
if (!isPlainRecord(value)) return { index, code: "invalid_record" };
|
|
55
67
|
if (Object.keys(value).some(key => !ALLOWED_RECORD_KEYS.has(key))) return { index, code: "invalid_record" };
|
|
56
|
-
if (!validEmail(value.email) || !validRefreshToken(value.refresh_token) || !validMetadata(value)) {
|
|
68
|
+
if (!validEmail(value.email) || !validRefreshToken(value.refresh_token) || !validProjectId(value.project_id) || !validMetadata(value)) {
|
|
57
69
|
return { index, code: "invalid_record" };
|
|
58
70
|
}
|
|
59
71
|
return {
|
|
@@ -61,6 +73,7 @@ function parseRecord(value: unknown, index: number): ParsedCockpitRecord {
|
|
|
61
73
|
record: {
|
|
62
74
|
email: value.email.toLowerCase(),
|
|
63
75
|
refreshToken: value.refresh_token,
|
|
76
|
+
...(typeof value.project_id === "string" ? { projectId: value.project_id } : {}),
|
|
64
77
|
},
|
|
65
78
|
};
|
|
66
79
|
}
|
|
@@ -11,6 +11,8 @@ export const ACCOUNT_IMPORT_MAX_REQUEST_BYTES = ACCOUNT_IMPORT_MAX_BYTES + 1024;
|
|
|
11
11
|
export const ACCOUNT_IMPORT_DEADLINE_MS = 600_000;
|
|
12
12
|
export const ACCOUNT_IMPORT_MAX_RECORDS = 25;
|
|
13
13
|
export const ACCOUNT_IMPORT_MAX_EMAIL_LENGTH = 254;
|
|
14
|
+
/** Google Cloud project ids are 6–30 characters; anything longer cannot name a project. */
|
|
15
|
+
export const ACCOUNT_IMPORT_MAX_PROJECT_ID_LENGTH = 30;
|
|
14
16
|
export const ACCOUNT_IMPORT_MAX_REFRESH_TOKEN_BYTES = 16 * 1024;
|
|
15
17
|
|
|
16
18
|
export type AccountImportStatus = "imported" | "updated" | "failed" | "unsupported";
|
|
@@ -65,6 +67,11 @@ export function throwIfAccountImportAborted(signal?: AbortSignal): void {
|
|
|
65
67
|
export interface CockpitAccountRecord {
|
|
66
68
|
email: string;
|
|
67
69
|
refreshToken: string;
|
|
70
|
+
/**
|
|
71
|
+
* Operator-supplied Cloud Code Assist project, used only when automatic discovery finds none
|
|
72
|
+
* and only after the account's own token proves it can use that project.
|
|
73
|
+
*/
|
|
74
|
+
projectId?: string;
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
export type ParsedCockpitRecord =
|
|
@@ -46,7 +46,16 @@ export function extractEmail(idToken?: string, accessToken?: string): string | u
|
|
|
46
46
|
if (!token) continue;
|
|
47
47
|
const payload = decodeJwtPayload(token);
|
|
48
48
|
if (!payload) continue;
|
|
49
|
-
if (typeof payload.email === "string"
|
|
49
|
+
if (typeof payload.email === "string" && payload.email.trim()) {
|
|
50
|
+
return payload.email.trim().toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
const profile = payload["https://api.openai.com/profile"];
|
|
53
|
+
if (profile && typeof profile === "object" && !Array.isArray(profile)) {
|
|
54
|
+
const email = (profile as Record<string, unknown>).email;
|
|
55
|
+
if (typeof email === "string" && email.trim()) {
|
|
56
|
+
return email.trim().toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
50
59
|
}
|
|
51
60
|
return undefined;
|
|
52
61
|
}
|
package/src/oauth/dsh.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
12
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
12
13
|
import { homedir } from "node:os";
|
|
13
14
|
import { join } from "node:path";
|
|
14
15
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -16,6 +17,20 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
16
17
|
export const DSH_PLATFORM_ORIGIN = "https://platform.deepseek.com";
|
|
17
18
|
export const DSH_INFERENCE_ORIGIN = "https://api.deepseek.com/anthropic";
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Byte ceiling for the profile document returned by /auth-api/v0/users/current.
|
|
22
|
+
*
|
|
23
|
+
* A legitimate profile is a small fixed-shape JSON object (code, data.biz_code,
|
|
24
|
+
* biz_data.id/email/id_profile{name,picture}) well under a few KiB; 64 KiB matches
|
|
25
|
+
* the shared bounded-body ceiling and still leaves orders of magnitude of
|
|
26
|
+
* headroom. Anything larger is rejected before parsing so a hostile or broken
|
|
27
|
+
* upstream cannot turn an import validation into unbounded memory work.
|
|
28
|
+
*/
|
|
29
|
+
export const DSH_PROFILE_MAX_BYTES = 64 * 1024;
|
|
30
|
+
|
|
31
|
+
/** Wall-clock budget for the profile request and its body read, in milliseconds. */
|
|
32
|
+
const DSH_PROFILE_FETCH_TIMEOUT_MS = 10_000;
|
|
33
|
+
|
|
19
34
|
export interface DshAccountDetectionResult {
|
|
20
35
|
detected: boolean;
|
|
21
36
|
token?: string;
|
|
@@ -80,7 +95,7 @@ export async function validateDshAccountToken(
|
|
|
80
95
|
signal?: AbortSignal,
|
|
81
96
|
): Promise<DshValidatedAccountIdentity> {
|
|
82
97
|
try {
|
|
83
|
-
const timeoutSignal = AbortSignal.timeout(
|
|
98
|
+
const timeoutSignal = AbortSignal.timeout(DSH_PROFILE_FETCH_TIMEOUT_MS);
|
|
84
99
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
85
100
|
const response = await fetch(`${DSH_PLATFORM_ORIGIN}/auth-api/v0/users/current`, {
|
|
86
101
|
method: "GET",
|
|
@@ -99,7 +114,35 @@ export async function validateDshAccountToken(
|
|
|
99
114
|
return { valid: false, error: `Platform returned HTTP ${response.status}` };
|
|
100
115
|
}
|
|
101
116
|
|
|
102
|
-
|
|
117
|
+
// A declared length past the cap is rejected without touching the stream.
|
|
118
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
119
|
+
if (Number.isFinite(declaredLength) && declaredLength > DSH_PROFILE_MAX_BYTES) {
|
|
120
|
+
try {
|
|
121
|
+
void response.body?.cancel(
|
|
122
|
+
new DOMException("DSH profile response is too large", "QuotaExceededError"),
|
|
123
|
+
).catch(() => undefined);
|
|
124
|
+
} catch {
|
|
125
|
+
// Best-effort cancellation only.
|
|
126
|
+
}
|
|
127
|
+
return { valid: false, error: "PROFILE_VALIDATION_FAILED" };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Missing or understated Content-Length still meets the cap on the decoded
|
|
131
|
+
// stream itself: once an over-limit byte is observed the retained buffer is
|
|
132
|
+
// discarded and cancellation is requested on the body. Malformed UTF-8 fails
|
|
133
|
+
// closed so a replacement character can never alter the authoritative ID.
|
|
134
|
+
const bounded = await readBoundedResponseBody(response, {
|
|
135
|
+
signal: combinedSignal,
|
|
136
|
+
maxBytes: DSH_PROFILE_MAX_BYTES,
|
|
137
|
+
totalTimeoutMs: DSH_PROFILE_FETCH_TIMEOUT_MS,
|
|
138
|
+
inactivityTimeoutMs: DSH_PROFILE_FETCH_TIMEOUT_MS,
|
|
139
|
+
fatalUtf8: true,
|
|
140
|
+
});
|
|
141
|
+
if (bounded.oversized || bounded.truncated || !bounded.displaySafe) {
|
|
142
|
+
return { valid: false, error: "PROFILE_VALIDATION_FAILED" };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const body = JSON.parse(bounded.text) as {
|
|
103
146
|
code?: number;
|
|
104
147
|
data?: {
|
|
105
148
|
biz_code?: number;
|
package/src/oauth/index.ts
CHANGED
|
@@ -1672,7 +1672,7 @@ export async function runLogin(
|
|
|
1672
1672
|
: existing.accountId && cred.accountId
|
|
1673
1673
|
? existing.accountId === cred.accountId
|
|
1674
1674
|
: existing.email && cred.email
|
|
1675
|
-
? existing.email.toLowerCase() === cred.email.toLowerCase()
|
|
1675
|
+
? existing.email.trim().toLowerCase() === cred.email.trim().toLowerCase()
|
|
1676
1676
|
: false;
|
|
1677
1677
|
if (!identityMatches) {
|
|
1678
1678
|
throw new OAuthReauthIdentityMismatchError();
|
package/src/oauth/kimi.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as os from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
7
|
+
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
7
8
|
import { getConfigDir } from "../config";
|
|
8
9
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
9
10
|
|
|
@@ -107,6 +108,7 @@ function getDeviceId(): string {
|
|
|
107
108
|
if ((e as { code?: string })?.code !== "ENOENT") throw e;
|
|
108
109
|
}
|
|
109
110
|
const id = randomUUID().replace(/-/g, "");
|
|
111
|
+
assertNotRealHomeUnderTest(getConfigDir());
|
|
110
112
|
recordOwnedConfigPath(getConfigDir(), p);
|
|
111
113
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
112
114
|
writeFileSync(p, id + "\n", { mode: 0o600 });
|
package/src/oauth/kiro.ts
CHANGED
|
@@ -205,11 +205,54 @@ function profileArnFromWhoami(parsed: Record<string, unknown>): string | undefin
|
|
|
205
205
|
: undefined);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* `kiro-cli whoami --format json` prints its identity as one JSON object, but once an IAM Identity
|
|
210
|
+
* Center profile is selected kiro-cli 2.32 follows that object with a human-readable `Profile:`
|
|
211
|
+
* block on stdout. Parsing the whole stream then failed, which silently dropped the user email that
|
|
212
|
+
* tells organization users apart (#6641). Only the leading top-level object is identity evidence;
|
|
213
|
+
* it must start the output and end its line, so text on later lines is ignored while a second
|
|
214
|
+
* value on the same line makes the identity unreadable.
|
|
215
|
+
*/
|
|
216
|
+
function parseWhoamiObject(stdout: string): Record<string, unknown> | undefined {
|
|
217
|
+
const start = stdout.search(/\S/);
|
|
218
|
+
if (start < 0 || stdout[start] !== "{") return undefined;
|
|
219
|
+
let depth = 0;
|
|
220
|
+
let inString = false;
|
|
221
|
+
let escaped = false;
|
|
222
|
+
for (let index = start; index < stdout.length; index++) {
|
|
223
|
+
const char = stdout[index];
|
|
224
|
+
if (inString) {
|
|
225
|
+
if (escaped) escaped = false;
|
|
226
|
+
else if (char === "\\") escaped = true;
|
|
227
|
+
else if (char === "\"") inString = false;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (char === "\"") inString = true;
|
|
231
|
+
else if (char === "{" || char === "[") depth++;
|
|
232
|
+
else if (char === "}" || char === "]") {
|
|
233
|
+
depth--;
|
|
234
|
+
if (depth > 0) continue;
|
|
235
|
+
if (!/^[ \t]*(?:\r?\n|$)/.test(stdout.slice(index + 1))) return undefined;
|
|
236
|
+
const parsed: unknown = JSON.parse(stdout.slice(start, index + 1));
|
|
237
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
238
|
+
? parsed as Record<string, unknown>
|
|
239
|
+
: undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Same-session identity reported by `kiro-cli whoami --format json`: the lower-cased user email
|
|
247
|
+
* and a structurally valid profile ARN. Any failure (non-zero exit, unreadable output, a thrown
|
|
248
|
+
* runner) yields an empty identity, so a login falls back to the SQLite import instead of failing.
|
|
249
|
+
*/
|
|
208
250
|
async function readKiroCliIdentity(runner: KiroCliRunner, signal?: AbortSignal): Promise<{ email?: string; profileArn?: string }> {
|
|
209
251
|
try {
|
|
210
252
|
const result = await runner(["whoami", "--format", "json"], signal);
|
|
211
253
|
if (result.exitCode !== 0) return {};
|
|
212
|
-
const parsed =
|
|
254
|
+
const parsed = parseWhoamiObject(result.stdout);
|
|
255
|
+
if (!parsed) return {};
|
|
213
256
|
const email = typeof parsed.email === "string" ? parsed.email.trim().toLowerCase() : "";
|
|
214
257
|
const profileArn = profileArnFromWhoami(parsed);
|
|
215
258
|
return {
|
|
@@ -297,14 +297,23 @@ export function clearLoginState(provider: string): void {
|
|
|
297
297
|
/**
|
|
298
298
|
* Cancel one login. `flowId` names it exactly; without one the provider's newest in-flight flow
|
|
299
299
|
* is cancelled, which is every single-flow caller (GUI, CLI) unchanged. A flow id belonging to a
|
|
300
|
-
* different provider is refused rather than cancelled.
|
|
300
|
+
* different provider is refused rather than cancelled. Only the public OAuth cancellation
|
|
301
|
+
* endpoint opts into failed-outcome dismissal; callers using this boolean as proof of an
|
|
302
|
+
* aborted controller (Codex flow ownership) retain their original contract.
|
|
301
303
|
*/
|
|
302
|
-
export function cancelLoginFlow(provider: string, flowId?: string): boolean {
|
|
304
|
+
export function cancelLoginFlow(provider: string, flowId?: string, options?: { dismissSettledFailure?: boolean }): boolean {
|
|
303
305
|
const flowKey = flowId ?? latestLoginFlowKey(provider);
|
|
304
306
|
if (flowKey === undefined) return false;
|
|
305
307
|
const active = loginAbort.get(flowKey);
|
|
306
308
|
const existing = loginState.get(flowKey);
|
|
307
309
|
if ((active?.provider ?? loginFlowProvider(flowKey)) !== provider) return false;
|
|
310
|
+
if (options?.dismissSettledFailure && !active && existing?.done && existing.error) {
|
|
311
|
+
// A failed add-account attempt does not invalidate a previously signed-in account. Let
|
|
312
|
+
// its owner dismiss the stale UI outcome without erasing credentials or another flow.
|
|
313
|
+
clearManualCodeSlot(flowKey);
|
|
314
|
+
dropLoginFlow(flowKey);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
308
317
|
if (!active && (!existing || existing.done)) return false;
|
|
309
318
|
abandonLoginFlow(flowKey, { error: "Login cancelled", errorCode: "no_pending_login" });
|
|
310
319
|
return true;
|
package/src/oauth/nous.ts
CHANGED
|
@@ -35,10 +35,11 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import { createHash } from "node:crypto";
|
|
37
37
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
38
|
-
import { join } from "node:path";
|
|
38
|
+
import { dirname, join } from "node:path";
|
|
39
39
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
40
40
|
import { getAuthStorePath } from "./store";
|
|
41
41
|
import { atomicWriteFile, hardenConfigDir, hardenExistingSecret } from "../config";
|
|
42
|
+
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
42
43
|
import { BOUNDED_BODY_MAX_BYTES, readBoundedResponseBytes } from "../lib/bounded-body";
|
|
43
44
|
|
|
44
45
|
export const NOUS_PORTAL_BASE_URL = "https://portal.nousresearch.com";
|
|
@@ -220,6 +221,8 @@ function readRefreshIntent(refreshToken: string): RefreshIntent | undefined {
|
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
function writeRefreshIntent(refreshToken: string, status: RefreshIntentStatus): void {
|
|
224
|
+
// Before the mkdir/chmod below, which would otherwise touch the real home first.
|
|
225
|
+
assertNotRealHomeUnderTest(dirname(getAuthStorePath()));
|
|
223
226
|
const dir = refreshIntentDir();
|
|
224
227
|
// Hardened, owner-only directory + atomic (temp+rename) write. Throws on
|
|
225
228
|
// failure so the caller can fail closed instead of refreshing blind.
|
package/src/oauth/store.ts
CHANGED
|
@@ -203,6 +203,7 @@ export function peekOAuthRefreshIntent(provider: string, accountId: string): OAu
|
|
|
203
203
|
}
|
|
204
204
|
export function writeOAuthRefreshIntent(provider: string, accountId: string, generation: string, createdAt = Date.now(), flightId?: string): OAuthRefreshIntent {
|
|
205
205
|
const dir = getConfigDir();
|
|
206
|
+
assertNotRealHomeUnderTest(dir);
|
|
206
207
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
207
208
|
hardenConfigDir();
|
|
208
209
|
const intent: OAuthRefreshIntent = {
|
|
@@ -1152,7 +1153,7 @@ export async function upsertCredentialByIdentity(
|
|
|
1152
1153
|
return Boolean(
|
|
1153
1154
|
safe.email
|
|
1154
1155
|
&& account.credential.email
|
|
1155
|
-
&& account.credential.email.toLowerCase() === safe.email.toLowerCase(),
|
|
1156
|
+
&& account.credential.email.trim().toLowerCase() === safe.email.trim().toLowerCase(),
|
|
1156
1157
|
);
|
|
1157
1158
|
};
|
|
1158
1159
|
const existing = safe.accountId
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* (`src/chat/inbound.ts`), including its validation and the fields it drops, which
|
|
7
7
|
* `src/protocols/features.ts` declares.
|
|
8
8
|
*/
|
|
9
|
-
import { chatCompletionsToResponsesBody, chatSettingsToResponsesBody } from "../../chat/inbound";
|
|
9
|
+
import { chatCompletionsToResponsesBody, chatSettingsToResponsesBody, type ChatResponsesProjectionOptions } from "../../chat/inbound";
|
|
10
10
|
import { featuresFromChatBody } from "../features";
|
|
11
11
|
|
|
12
12
|
/** Project a Chat Completions body onto the internal Responses bridge body. */
|
|
13
|
-
export function chatToResponsesBody(body: unknown): Record<string, unknown> {
|
|
14
|
-
return chatCompletionsToResponsesBody(body);
|
|
13
|
+
export function chatToResponsesBody(body: unknown, options?: ChatResponsesProjectionOptions): Record<string, unknown> {
|
|
14
|
+
return chatCompletionsToResponsesBody(body, options);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -109,7 +109,13 @@ export interface ClientWireWriter {
|
|
|
109
109
|
text(delta: string): void;
|
|
110
110
|
messageDone(): void;
|
|
111
111
|
reasoning(delta: string, itemId: string, channel: "summary" | "raw"): void;
|
|
112
|
-
|
|
112
|
+
/** `hidden`: the signature covers thinking text the client was not shown (the bridge's `txt`). */
|
|
113
|
+
reasoningDone(item: { itemId: string; signature?: string; redacted?: string[]; hidden?: true }): void;
|
|
114
|
+
/**
|
|
115
|
+
* An Anthropic progress update: `text` is shown as content and is also the signed thinking
|
|
116
|
+
* block's text. Optional — a writer without it renders the update as ordinary text.
|
|
117
|
+
*/
|
|
118
|
+
progressThinking?(text: string, signature: string): void;
|
|
113
119
|
toolStart(call: ClientToolCall): void;
|
|
114
120
|
toolArgsDelta(itemId: string, delta: string): void;
|
|
115
121
|
toolArgsDone(itemId: string, args: string): void;
|
|
@@ -331,6 +337,8 @@ export function encodeAdapterEventStream(
|
|
|
331
337
|
let currentRawReasoning: { itemId: string } | null = null;
|
|
332
338
|
let pendingSignature: string | undefined;
|
|
333
339
|
let pendingRedacted: string[] = [];
|
|
340
|
+
// The bridge's hiddenThinking: non-empty thinking text suppressed by hideThinkingSummary.
|
|
341
|
+
let hiddenThinkingSeen = false;
|
|
334
342
|
let hiddenRawBytes = 0;
|
|
335
343
|
let pendingKiroRedacted: string | undefined;
|
|
336
344
|
let currentToolCall: OpenToolCall | null = null;
|
|
@@ -367,9 +375,11 @@ export function encodeAdapterEventStream(
|
|
|
367
375
|
};
|
|
368
376
|
// hideThinkingSummary: a signed or redacted block still round-trips as an envelope-only item.
|
|
369
377
|
const flushHiddenReasoningEnvelope = () => {
|
|
378
|
+
const hidden = hiddenThinkingSeen;
|
|
379
|
+
hiddenThinkingSeen = false;
|
|
370
380
|
const envelope = takeReasoningEnvelope();
|
|
371
381
|
if (!envelope) return;
|
|
372
|
-
writer.reasoningDone({ itemId: `rs_${uuid()}`, ...envelope });
|
|
382
|
+
writer.reasoningDone({ itemId: `rs_${uuid()}`, ...envelope, ...(hidden ? { hidden: true as const } : {}) });
|
|
373
383
|
};
|
|
374
384
|
const flushHiddenRawReasoning = () => {
|
|
375
385
|
if (hiddenRawBytes === 0) return;
|
|
@@ -570,6 +580,19 @@ export function encodeAdapterEventStream(
|
|
|
570
580
|
break;
|
|
571
581
|
}
|
|
572
582
|
case "text_delta": {
|
|
583
|
+
if (event.anthropicThinkingSignature && writer.progressThinking) {
|
|
584
|
+
// The bridge's progress items: close everything, the carrier, then the visible copy.
|
|
585
|
+
if (currentMsg) closeCurrentMessage();
|
|
586
|
+
if (currentReasoning) closeCurrentReasoning();
|
|
587
|
+
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
588
|
+
flushHiddenRawReasoning();
|
|
589
|
+
flushHiddenReasoningEnvelope();
|
|
590
|
+
if (currentToolCall) closeCurrentToolCall();
|
|
591
|
+
writer.progressThinking(event.text, event.anthropicThinkingSignature);
|
|
592
|
+
writer.text(event.text);
|
|
593
|
+
writer.messageDone();
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
573
596
|
if (currentReasoning) closeCurrentReasoning();
|
|
574
597
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
575
598
|
flushHiddenRawReasoning();
|
|
@@ -585,6 +608,7 @@ export function encodeAdapterEventStream(
|
|
|
585
608
|
}
|
|
586
609
|
case "thinking_delta": {
|
|
587
610
|
if (options.hideThinkingSummary) {
|
|
611
|
+
if (event.thinking.length > 0) hiddenThinkingSeen = true;
|
|
588
612
|
flushHiddenRawReasoning();
|
|
589
613
|
break;
|
|
590
614
|
}
|
|
@@ -13,6 +13,7 @@ import type { AdapterEvent } from "../../types";
|
|
|
13
13
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
14
14
|
import { responsesUsage } from "../../bridge/internal";
|
|
15
15
|
import {
|
|
16
|
+
appendedUtf8Bytes,
|
|
16
17
|
chatCompletionsErrorResponse,
|
|
17
18
|
chatCompletionsFailedResponse,
|
|
18
19
|
chatCompletionsIncompleteOutcome,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
type ClientWireWriter,
|
|
33
34
|
} from "./adapter-events";
|
|
34
35
|
import type { RelayedEventObservation } from "../../usage/attempt-delivery";
|
|
36
|
+
import { reasoningDetailEntries } from "../../chat/reasoning-details";
|
|
35
37
|
|
|
36
38
|
type Rec = Record<string, unknown>;
|
|
37
39
|
|
|
@@ -54,6 +56,41 @@ function createChatCompletionWriter(sink: ClientFrameSink, model: string): Clien
|
|
|
54
56
|
const emittedToolIndexes = new Set<number>();
|
|
55
57
|
let nextToolIndex = 0;
|
|
56
58
|
let staged: { text: string; observation?: RelayedEventObservation }[] | undefined;
|
|
59
|
+
// #7429: the shown thinking text of the open reasoning item, which its signature pairs with.
|
|
60
|
+
const budget = sink.budget;
|
|
61
|
+
let reasoningItemId: string | undefined;
|
|
62
|
+
let reasoningText = "";
|
|
63
|
+
let reasoningTextBytes = 0;
|
|
64
|
+
let nextReasoningDetailIndex = 0;
|
|
65
|
+
const releaseReasoningText = () => {
|
|
66
|
+
budget.releaseRetained(reasoningTextBytes, { kind: "reasoning" });
|
|
67
|
+
reasoningItemId = undefined;
|
|
68
|
+
reasoningText = "";
|
|
69
|
+
reasoningTextBytes = 0;
|
|
70
|
+
};
|
|
71
|
+
const retainReasoningText = (itemId: string, delta: string) => {
|
|
72
|
+
if (reasoningItemId !== itemId) releaseReasoningText();
|
|
73
|
+
reasoningItemId = itemId;
|
|
74
|
+
const nextBytes = appendedUtf8Bytes(reasoningText, reasoningTextBytes, delta);
|
|
75
|
+
const reservation = budget.reserveTransient(nextBytes, { kind: "reasoning" });
|
|
76
|
+
try {
|
|
77
|
+
reasoningText += delta;
|
|
78
|
+
reservation.commitRetained();
|
|
79
|
+
budget.releaseRetained(reasoningTextBytes, { kind: "reasoning" });
|
|
80
|
+
reasoningTextBytes = nextBytes;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
reservation.release();
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const emitReasoningDetails = (entries: Rec[]) => {
|
|
87
|
+
if (entries.length === 0) return;
|
|
88
|
+
nextReasoningDetailIndex += entries.length;
|
|
89
|
+
ensureRole();
|
|
90
|
+
const frame = chunkBase(id, model, created);
|
|
91
|
+
frame.choices = [{ index: 0, delta: { reasoning_details: entries }, finish_reason: null }];
|
|
92
|
+
emit(frame);
|
|
93
|
+
};
|
|
57
94
|
|
|
58
95
|
const emit = (payload: Rec | "[DONE]", observation?: RelayedEventObservation) => {
|
|
59
96
|
if (failed) return;
|
|
@@ -138,15 +175,30 @@ function createChatCompletionWriter(sink: ClientFrameSink, model: string): Clien
|
|
|
138
175
|
emit(frame, { semanticBytes: Buffer.byteLength(delta) });
|
|
139
176
|
},
|
|
140
177
|
messageDone() { /* the converter reads nothing from a finished message */ },
|
|
141
|
-
reasoning(delta) {
|
|
178
|
+
reasoning(delta, itemId, channel) {
|
|
142
179
|
if (!delta) return;
|
|
143
180
|
ensureRole();
|
|
144
181
|
// Many OpenAI-compatible clients accept reasoning_content; harmless if ignored.
|
|
145
182
|
const frame = chunkBase(id, model, created);
|
|
146
183
|
frame.choices = [{ index: 0, delta: { reasoning_content: delta }, finish_reason: null }];
|
|
147
184
|
emit(frame, { semanticBytes: Buffer.byteLength(delta) });
|
|
185
|
+
// Only summary-channel thinking can carry a signature; raw reasoning never does.
|
|
186
|
+
if (channel === "summary") retainReasoningText(itemId, delta);
|
|
187
|
+
},
|
|
188
|
+
reasoningDone({ itemId, signature, redacted, hidden }) {
|
|
189
|
+
// The bridge converter's output_item.done for a reasoning item (#7429), entry for entry.
|
|
190
|
+
const entries = reasoningDetailEntries({
|
|
191
|
+
text: reasoningItemId === itemId ? reasoningText : "",
|
|
192
|
+
...(signature ? { signature } : {}),
|
|
193
|
+
...(redacted ? { redacted } : {}),
|
|
194
|
+
...(hidden ? { hidden } : {}),
|
|
195
|
+
}, nextReasoningDetailIndex);
|
|
196
|
+
releaseReasoningText();
|
|
197
|
+
emitReasoningDetails(entries);
|
|
198
|
+
},
|
|
199
|
+
progressThinking(text, signature) {
|
|
200
|
+
emitReasoningDetails(reasoningDetailEntries({ text, signature }, nextReasoningDetailIndex));
|
|
148
201
|
},
|
|
149
|
-
reasoningDone() { /* reasoning items carry nothing a Chat client reads */ },
|
|
150
202
|
toolStart(call) {
|
|
151
203
|
// Custom and tool-search calls have no Chat representation; the converter skips them.
|
|
152
204
|
if (call.kind !== "function") return;
|
|
@@ -198,7 +250,7 @@ function createChatCompletionWriter(sink: ClientFrameSink, model: string): Clien
|
|
|
198
250
|
type: "upstream_error",
|
|
199
251
|
});
|
|
200
252
|
},
|
|
201
|
-
dispose() {
|
|
253
|
+
dispose() { releaseReasoningText(); },
|
|
202
254
|
};
|
|
203
255
|
}
|
|
204
256
|
|
|
@@ -4,7 +4,8 @@ import { bindRawDecisionWindows } from "./quota-decision-publication";
|
|
|
4
4
|
import { decisionQuotaSinkActive } from "../lib/decision-quota-sink";
|
|
5
5
|
import { CLAUDE_CLI_USER_AGENT } from "./claude-cli-identity";
|
|
6
6
|
import { asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TIMEOUT_MS } from "./quota-wire";
|
|
7
|
-
import type { ProviderQuota, ProviderQuotaWindow } from "./quota-types";
|
|
7
|
+
import type { ProviderQuota, ProviderQuotaWindow, QuotaFailureCode } from "./quota-types";
|
|
8
|
+
import { httpQuotaFailure } from "./quota/failure-codes";
|
|
8
9
|
const hasQuotaRows = (q: ProviderQuota) => q.fiveHourPercent !== undefined || q.weeklyPercent !== undefined || Boolean(q.customWindows?.length);
|
|
9
10
|
|
|
10
11
|
function parseClaudeBucket(value: unknown): { percent?: number; resetAt?: number } | null {
|
|
@@ -46,6 +47,16 @@ function parseClaudeLimit(value: unknown): ProviderQuotaWindow | null {
|
|
|
46
47
|
|
|
47
48
|
/** Read the Anthropic OAuth usage endpoint for an access token and return the parsed quota, or null on a non-OK or malformed response. */
|
|
48
49
|
export async function readAnthropicUsageQuota(accessToken: string): Promise<ProviderQuota | null> {
|
|
50
|
+
return (await probeAnthropicUsageQuota(accessToken)).quota;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A usage read plus, when it yields no quota, the closed reason why. A thrown fetch still throws. */
|
|
54
|
+
export interface AnthropicUsageProbe {
|
|
55
|
+
quota: ProviderQuota | null;
|
|
56
|
+
failure?: QuotaFailureCode;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function probeAnthropicUsageQuota(accessToken: string): Promise<AnthropicUsageProbe> {
|
|
49
60
|
const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
50
61
|
headers: {
|
|
51
62
|
Accept: "application/json, text/plain, */*",
|
|
@@ -56,9 +67,12 @@ export async function readAnthropicUsageQuota(accessToken: string): Promise<Prov
|
|
|
56
67
|
},
|
|
57
68
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
58
69
|
});
|
|
59
|
-
if (!response.ok)
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
try { await response.body?.cancel(); } catch { /* Best-effort release; the status is the diagnosis. */ }
|
|
72
|
+
return { quota: null, failure: httpQuotaFailure(response.status) };
|
|
73
|
+
}
|
|
60
74
|
const body = asRecord(await readQuotaJson(response));
|
|
61
|
-
if (!body) return null;
|
|
75
|
+
if (!body) return { quota: null, failure: "response_unusable" };
|
|
62
76
|
const fiveHour = parseClaudeBucket(body.five_hour);
|
|
63
77
|
const sevenDay = parseClaudeBucket(body.seven_day);
|
|
64
78
|
const fable = parseClaudeBucket(body.seven_day_fable);
|
|
@@ -93,9 +107,10 @@ export async function readAnthropicUsageQuota(accessToken: string): Promise<Prov
|
|
|
93
107
|
};
|
|
94
108
|
if (decisionQuotaSinkActive()) bindRawDecisionWindows(quota, rawAnthropicUsageWindows(body, quota.updatedAt));
|
|
95
109
|
// Empty / schema-changed payloads must not cache as "success with no bars".
|
|
96
|
-
|
|
110
|
+
if (!hasQuotaRows(quota)) return { quota: null, failure: "response_unusable" };
|
|
111
|
+
return { quota: markAnthropicFamilyEnumeration(quota, Array.isArray(body.limits) && body.limits.every(raw => {
|
|
97
112
|
const kind = asRecord(raw)?.kind;
|
|
98
113
|
return kind === "session" || kind === "weekly_all" || kind === "weekly_scoped" && parseClaudeLimit(raw) !== null;
|
|
99
|
-
}))
|
|
114
|
+
})) };
|
|
100
115
|
}
|
|
101
116
|
|
|
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { getProviderRegistryEntry, providerMatchesRegistryTransport } from "./registry";
|
|
4
4
|
import { routedSlug, slugEquivalenceKey } from "./slug-codec";
|
|
5
5
|
import { comboDisabledModelSelectors } from "../combos/types";
|
|
6
|
-
import { providerUsesKeyAuthOverride
|
|
6
|
+
import { providerUsesKeyAuthOverride } from "./key-store";
|
|
7
7
|
|
|
8
8
|
export const INITIAL_MODEL_SELECTION_THRESHOLD = 20;
|
|
9
9
|
type Selection = NonNullable<OcxProviderConfig["initialModelSelection"]>;
|
|
@@ -31,7 +31,7 @@ function loginConnection(name: string, provider: OcxProviderConfig): boolean {
|
|
|
31
31
|
if (entry && providerMatchesRegistryTransport(name, provider)) {
|
|
32
32
|
if (entry.authKind === "forward") return true;
|
|
33
33
|
if (entry.authKind === "oauth") {
|
|
34
|
-
return !providerUsesKeyAuthOverride(entry, provider
|
|
34
|
+
return !providerUsesKeyAuthOverride(entry, provider);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
return provider.authMode === "oauth" || provider.authMode === "forward";
|