@bitkyc08/opencodex 2.90.0 → 2.92.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/bin/ocx.mjs +37 -3
- package/gui/dist/assets/App-DMps4OLk.css +1 -0
- package/gui/dist/assets/{App-DgxkE6Md.js → App-sUf_8afJ.js} +18 -18
- package/gui/dist/assets/{Tray-DbTNc_88.js → Tray-BSsxznDm.js} +1 -1
- package/gui/dist/assets/index-BlFE_VB_.js +94 -0
- package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
- package/package.json +4 -3
- package/src/adapters/anthropic.ts +2 -5
- package/src/adapters/claude-cli/usage-admission.ts +14 -0
- package/src/adapters/google-vertex-endpoint.ts +9 -1
- package/src/adapters/google-vertex-key-project.ts +223 -0
- package/src/adapters/google.ts +13 -3
- package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/passthrough.ts +2 -1
- package/src/adapters/openai-responses/request-strips.ts +92 -0
- package/src/adapters/openai-responses/transport.ts +6 -0
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/workbuddy-schema.ts +61 -0
- package/src/adapters/workbuddy.ts +68 -0
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +1 -1
- package/src/chat/inbound.ts +20 -2
- package/src/chat/outbound.ts +47 -8
- package/src/chat/reasoning-details.ts +137 -0
- package/src/cli/capabilities-agents-routing.ts +53 -3
- package/src/cli/capabilities-base.ts +15 -2
- package/src/cli/capabilities-observe-system.ts +4 -4
- package/src/cli/codex-cli-update.ts +8 -0
- package/src/cli/codex-shim-autorestore.ts +5 -0
- package/src/cli/combo-result.ts +6 -0
- package/src/cli/combo-stats.ts +3 -0
- package/src/cli/combo.ts +66 -42
- package/src/cli/dispatch.ts +30 -0
- package/src/cli/fallback.ts +127 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +1 -2
- package/src/cli/launcher-context.ts +49 -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 +123 -35
- package/src/cli/provider.ts +46 -3
- 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/cli/usage-compare.ts +68 -12
- 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-ingress.ts +1 -1
- package/src/client/link-parity-reporter.ts +107 -0
- package/src/client/link-readiness.ts +97 -0
- package/src/client/link-relay-transport.ts +84 -9
- package/src/client/link-relay.ts +98 -15
- package/src/client/link-tunnel.ts +45 -18
- package/src/client/machine-listener.ts +103 -3
- package/src/client/runtime-restart.ts +127 -0
- package/src/client/runtime.ts +155 -25
- package/src/codex/account-store.ts +17 -3
- package/src/codex/app-server-restart-service.ts +6 -5
- package/src/codex/auth-context.ts +10 -6
- package/src/codex/catalog/effort.ts +9 -2
- package/src/codex/catalog/gather-capture.ts +4 -2
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/provider-models.ts +14 -8
- package/src/codex/chatgpt-refresh-failure.ts +6 -2
- package/src/codex/cli-install-provenance.ts +418 -0
- package/src/codex/cli-installation-targets.ts +5 -1
- package/src/codex/cli-update-plan.ts +43 -16
- package/src/codex/desktop-app/picker-freshness.ts +10 -8
- package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
- package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
- package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
- package/src/codex/pool-refresh-backoff.ts +3 -1
- package/src/codex/quota-history.ts +7 -0
- package/src/codex/quota.ts +10 -0
- package/src/codex/subagent-model-fallback.ts +24 -1
- package/src/codex/user-identity.ts +35 -6
- package/src/codex/windows-current-sid.ts +162 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/jev-quota-activation.ts +5 -1
- package/src/combos/jev-quota-route.ts +1 -1
- package/src/combos/jev-quota.ts +9 -1
- package/src/combos/jev-session-pin-config.ts +51 -0
- package/src/combos/jev.ts +6 -0
- package/src/combos/resolve.ts +12 -0
- package/src/combos/types.ts +19 -0
- package/src/config/diagnostics.ts +5 -0
- package/src/config/load-degrade.ts +2 -0
- package/src/config/provider-validation.ts +7 -0
- package/src/config/schema/config-schema.ts +3 -0
- package/src/config/schema/fallback-policy.ts +40 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +375 -195
- package/src/lib/authorization-scheme.ts +95 -0
- package/src/lib/decision-quota-sink.ts +10 -0
- package/src/lib/provider-client-headers.ts +1 -0
- package/src/lib/redact.ts +2 -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 +4 -4
- package/src/lib/upstream-retry.ts +4 -1
- 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/diagnostics.ts +193 -0
- package/src/link/management-routes.ts +2 -0
- package/src/link/peer-report.ts +100 -0
- package/src/link/relay-auth.ts +6 -0
- package/src/link/remote-platform.ts +78 -0
- package/src/link/ssh-argv.ts +19 -0
- package/src/link/status-projection.ts +63 -2
- package/src/link/supervisor.ts +45 -12
- package/src/link/tunnel-history.ts +109 -0
- package/src/link/tunnel-state.ts +80 -17
- package/src/messaging/remote-files.ts +24 -1
- package/src/oauth/generic-account-failover.ts +91 -7
- package/src/oauth/index.ts +16 -1
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +5 -0
- package/src/oauth/nous.ts +4 -1
- package/src/oauth/store.ts +6 -2
- package/src/oauth/token-guardian.ts +7 -0
- package/src/oauth/types.ts +9 -0
- package/src/oauth/workbuddy.ts +139 -0
- 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/antigravity-quota-identity.ts +37 -0
- package/src/providers/base-url-choices.ts +8 -5
- package/src/providers/derive.ts +23 -3
- package/src/providers/mistral-transport.ts +37 -4
- package/src/providers/model-discovery.ts +2 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/quota/account-cache.ts +3 -2
- package/src/providers/quota/antigravity.ts +27 -22
- package/src/providers/quota/report-cache.ts +13 -4
- package/src/providers/quota/vendor-probes-key.ts +60 -0
- package/src/providers/quota/workbuddy.ts +83 -0
- package/src/providers/quota-decision-antigravity.ts +64 -0
- package/src/providers/quota-decision-publication.ts +11 -0
- package/src/providers/quota-decision-snapshot.ts +21 -4
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +3 -1
- package/src/providers/registry/entries-core.ts +9 -0
- package/src/providers/registry/entries-extended.ts +20 -7
- package/src/providers/registry/model-ids.ts +3 -0
- package/src/providers/registry/model-seeds.ts +27 -28
- package/src/providers/registry/types.ts +19 -1
- package/src/providers/resolved-model-policy.ts +4 -4
- package/src/providers/routed-config.ts +15 -0
- package/src/providers/stale-model-roster-migration.ts +83 -15
- package/src/providers/workbuddy-credit-balance.ts +81 -0
- package/src/providers/workbuddy-models.ts +37 -0
- package/src/providers/workbuddy.ts +60 -0
- 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/parser.ts +7 -0
- package/src/responses/reasoning-envelope.ts +12 -0
- package/src/responses/reasoning-replay-cache.ts +18 -14
- package/src/router.ts +2 -0
- package/src/routing/analytics.ts +2 -3
- 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 +7 -5
- package/src/server/background-lifecycle.ts +4 -0
- package/src/server/chat-completions.ts +22 -2
- package/src/server/chat-native-reasoning.ts +10 -3
- package/src/server/chat-native.ts +13 -2
- package/src/server/claude-messages.ts +3 -0
- 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 +20 -7
- package/src/server/index/optional-listeners.ts +11 -2
- package/src/server/index/serve-options.ts +19 -2
- package/src/server/live.ts +5 -0
- package/src/server/local-management-read-client.ts +3 -1
- package/src/server/management/combo-routes.ts +10 -4
- 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 +16 -2
- package/src/server/management/provider-routes.ts +1 -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/port-reclaim.ts +51 -9
- package/src/server/request-log-conversation.ts +49 -0
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
- package/src/server/responses/compact.ts +12 -4
- package/src/server/responses/core-auth.ts +11 -1
- package/src/server/responses/core-combo.ts +125 -32
- package/src/server/responses/core-lifetime.ts +2 -2
- package/src/server/responses/core-normalize.ts +6 -3
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +2 -1
- package/src/server/responses/encrypted-payload.ts +3 -3
- package/src/server/responses/jev-combo-choice.ts +39 -0
- package/src/server/responses/jev-session-pin.ts +203 -0
- package/src/server/responses/main-hard-lock-routing.ts +54 -2
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/policy-fallback.ts +44 -1
- package/src/server/responses/request-prepare.ts +19 -6
- package/src/server/responses/request-transport.ts +25 -6
- package/src/server/responses/run-turn-execution.ts +2 -4
- package/src/server/responses/sidecar-execution.ts +2 -4
- package/src/server/responses-phase-inference.ts +267 -0
- package/src/server/restart-replacement.ts +50 -2
- package/src/server/sse-payload-rewrite.ts +14 -1
- package/src/server/ws-bridge.ts +5 -0
- package/src/service/cli.ts +25 -2
- package/src/service/definition.ts +20 -8
- package/src/service/diagnostics.ts +20 -6
- package/src/service/status-json.ts +116 -0
- package/src/types/config.ts +13 -0
- package/src/types/fallback.ts +38 -0
- package/src/types/provider.ts +9 -1
- package/src/types/request.ts +1 -0
- package/src/types/wire.ts +1 -0
- package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/usage/jev-stats.ts +54 -14
- package/gui/dist/assets/App-dnkJ85te.css +0 -1
- package/gui/dist/assets/index-TmaHa54u.js +0 -94
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +0 -1
package/src/oauth/index.ts
CHANGED
|
@@ -50,6 +50,8 @@ import { validateDevinApiBaseUrl } from "./devin/api-base";
|
|
|
50
50
|
import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot";
|
|
51
51
|
import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
|
|
52
52
|
import { loginMetaMuse, refreshMetaMuseToken } from "./meta-muse";
|
|
53
|
+
import { loginWorkBuddy, refreshWorkBuddyToken } from "./workbuddy";
|
|
54
|
+
import { WORKBUDDY_CONFIG_URL, workBuddyHeaders } from "../providers/workbuddy";
|
|
53
55
|
import { DSH_ACCOUNT_AUTH } from "./dsh";
|
|
54
56
|
import { loginOrcaRouter, orcaRouterInferenceBaseUrl, refreshOrcaRouterKey } from "./orcarouter";
|
|
55
57
|
import { ANTIGRAVITY_DISCOVERY_UA } from "../adapters/google-antigravity-wire";
|
|
@@ -98,6 +100,7 @@ import { normalizeOAuthPlan, mergeOAuthPlan } from "./plan";
|
|
|
98
100
|
import { mergeAnthropicIdentity, type AnthropicIdentityResolver } from "./anthropic-identity";
|
|
99
101
|
import { AnthropicCrossInstanceDuplicateError, AnthropicLocalCliImportError, assertAnthropicCredentialSource } from "./store-anthropic-instance";
|
|
100
102
|
export interface OAuthAccessSnapshot {
|
|
103
|
+
workbuddy?: import("./types").WorkBuddyOAuthMetadata;
|
|
101
104
|
provider: string;
|
|
102
105
|
accountId: string;
|
|
103
106
|
generation: string;
|
|
@@ -246,6 +249,11 @@ function oauthDefaultModel(id: string): string {
|
|
|
246
249
|
}
|
|
247
250
|
|
|
248
251
|
export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
|
|
252
|
+
"workbuddy-ai": {
|
|
253
|
+
login: loginWorkBuddy, refresh: refreshWorkBuddyToken,
|
|
254
|
+
providerConfig: oauthConfig("workbuddy-ai"), defaultModel: oauthDefaultModel("workbuddy-ai"),
|
|
255
|
+
defaultRefreshPolicy: "lazy-only",
|
|
256
|
+
},
|
|
249
257
|
"dsh-account": {
|
|
250
258
|
...DSH_ACCOUNT_AUTH,
|
|
251
259
|
providerConfig: oauthConfig("dsh-account"),
|
|
@@ -561,6 +569,7 @@ function accessSnapshot(provider: string, accountId: string, cred: OAuthCredenti
|
|
|
561
569
|
accountId,
|
|
562
570
|
generation: credentialGeneration(cred),
|
|
563
571
|
accessToken: cred.access,
|
|
572
|
+
...(oauthProvider === "workbuddy-ai" && cred.workbuddy ? { workbuddy: { ...cred.workbuddy } } : {}),
|
|
564
573
|
...(cred.projectId ? { projectId: cred.projectId } : {}),
|
|
565
574
|
...(accountApiBaseUrl ? { apiBaseUrl: accountApiBaseUrl } : {}),
|
|
566
575
|
...(oauthProvider === "zed" && cred.accountId ? { providerUserId: cred.accountId } : {}),
|
|
@@ -717,6 +726,7 @@ export async function getValidAccessTokenSnapshot(
|
|
|
717
726
|
|
|
718
727
|
/** Providers whose upstream-401 replay path may force a snapshot refresh. */
|
|
719
728
|
const FORCE_REFRESH_PROVIDERS = new Set([
|
|
729
|
+
"workbuddy-ai",
|
|
720
730
|
"xai",
|
|
721
731
|
"github-copilot",
|
|
722
732
|
"kiro",
|
|
@@ -1177,6 +1187,7 @@ function modelDiscoveryTransportSeed(providerName: string, prov: OcxProviderConf
|
|
|
1177
1187
|
* `/models` + Bearer with a `{ data: [{ id, owned_by? }] }` response.
|
|
1178
1188
|
*/
|
|
1179
1189
|
export interface ModelsRequestObservedAuth {
|
|
1190
|
+
readonly workbuddy?: import("./types").WorkBuddyOAuthMetadata;
|
|
1180
1191
|
readonly oauthApiBaseUrl?: string;
|
|
1181
1192
|
}
|
|
1182
1193
|
|
|
@@ -1186,6 +1197,10 @@ export function buildModelsRequest(
|
|
|
1186
1197
|
providerName = "",
|
|
1187
1198
|
observedAuth?: ModelsRequestObservedAuth,
|
|
1188
1199
|
): { method?: "POST"; url: string; headers: Record<string, string> } {
|
|
1200
|
+
if (providerName === "workbuddy-ai" || prov.adapter === "workbuddy-ai") {
|
|
1201
|
+
const allowed = mayResolveModelsOAuth(providerName, prov);
|
|
1202
|
+
return { url: WORKBUDDY_CONFIG_URL, headers: workBuddyHeaders(allowed && observedAuth?.workbuddy ? apiKey : undefined, observedAuth?.workbuddy) };
|
|
1203
|
+
}
|
|
1189
1204
|
const authorizedTarget = captureModelsOAuthTarget(providerName, prov);
|
|
1190
1205
|
if (prov.authMode === "oauth" && !mayResolveModelsOAuth(providerName, prov)) apiKey = undefined;
|
|
1191
1206
|
const transportSeed = modelDiscoveryTransportSeed(providerName, prov);
|
|
@@ -1672,7 +1687,7 @@ export async function runLogin(
|
|
|
1672
1687
|
: existing.accountId && cred.accountId
|
|
1673
1688
|
? existing.accountId === cred.accountId
|
|
1674
1689
|
: existing.email && cred.email
|
|
1675
|
-
? existing.email.toLowerCase() === cred.email.toLowerCase()
|
|
1690
|
+
? existing.email.trim().toLowerCase() === cred.email.trim().toLowerCase()
|
|
1676
1691
|
: false;
|
|
1677
1692
|
if (!identityMatches) {
|
|
1678
1693
|
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 });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertWorkBuddyDestination } from "../providers/workbuddy";
|
|
1
2
|
import type { OcxProviderConfig } from "../types";
|
|
2
3
|
import { isBuiltinAnthropicInstanceRow } from "../providers/anthropic-instance";
|
|
3
4
|
import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
|
|
@@ -8,6 +9,10 @@ export function mayResolveModelsOAuth(name: string, provider: OcxProviderConfig
|
|
|
8
9
|
// The DSH account token is pinned to its own adapter and fixed DeepSeek hosts. Its catalog is
|
|
9
10
|
// static, so no discovery path may resolve it, whatever adapter or baseUrl the row was edited to.
|
|
10
11
|
if (name === "dsh-account") return false;
|
|
12
|
+
if (name === "workbuddy-ai" || provider?.adapter === "workbuddy-ai") {
|
|
13
|
+
if (name !== "workbuddy-ai" || provider?.adapter !== "workbuddy-ai" || provider.disabled) return false;
|
|
14
|
+
try { assertWorkBuddyDestination(provider.baseUrl); return true; } catch { return false; }
|
|
15
|
+
}
|
|
11
16
|
return name !== "anthropic2" || (provider !== undefined && provider.disabled !== true
|
|
12
17
|
&& isBuiltinAnthropicInstanceRow(name, provider));
|
|
13
18
|
}
|
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
|
@@ -23,6 +23,7 @@ import { notifyAuthStoreLoaded } from "../lib/decision-quota-sink";
|
|
|
23
23
|
import { accountPauseDetailsSchema } from "../config/schema/account-auto-pause";
|
|
24
24
|
import { normalizeOAuthPlan, mergeOAuthPlan } from "./plan";
|
|
25
25
|
import { normalizeAnthropicIdentity, matchesAnthropicImport, mergeAnthropicIdentity } from "./anthropic-identity";
|
|
26
|
+
import { normalizeWorkBuddyMetadata } from "../providers/workbuddy";
|
|
26
27
|
import { findKiroAccountForLogin, withRetainedKiroUser } from "./kiro-account-identity";
|
|
27
28
|
import { isAnthropicInstanceId, type AnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
28
29
|
import { assertNoCrossAnthropicRegistration, assertAnthropicCredentialSource, assertAnthropicInstanceLoginConfig } from "./store-anthropic-instance";
|
|
@@ -203,6 +204,7 @@ export function peekOAuthRefreshIntent(provider: string, accountId: string): OAu
|
|
|
203
204
|
}
|
|
204
205
|
export function writeOAuthRefreshIntent(provider: string, accountId: string, generation: string, createdAt = Date.now(), flightId?: string): OAuthRefreshIntent {
|
|
205
206
|
const dir = getConfigDir();
|
|
207
|
+
assertNotRealHomeUnderTest(dir);
|
|
206
208
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
207
209
|
hardenConfigDir();
|
|
208
210
|
const intent: OAuthRefreshIntent = {
|
|
@@ -353,7 +355,7 @@ export function clearOAuthRefreshIntent(provider: string, accountId: string, gen
|
|
|
353
355
|
});
|
|
354
356
|
}
|
|
355
357
|
export function credentialGeneration(cred: OAuthCredentials): string {
|
|
356
|
-
return createHash("sha256").update(JSON.stringify([cred.refresh, cred.access, cred.expires])).digest("hex");
|
|
358
|
+
return createHash("sha256").update(JSON.stringify([cred.refresh, cred.access, cred.expires, ...(cred.workbuddy ? [cred.workbuddy] : [])])).digest("hex");
|
|
357
359
|
}
|
|
358
360
|
|
|
359
361
|
/** Load and normalize the OAuth auth store with hardened permissions, and hand the result to the opt-in decision-quota slot (a no-op unless JEV quota signals are active). */
|
|
@@ -547,6 +549,8 @@ function normalizeCredential(cred: unknown): OAuthCredentials | null {
|
|
|
547
549
|
};
|
|
548
550
|
if (typeof candidate.email === "string" && candidate.email.length > 0) normalized.email = candidate.email;
|
|
549
551
|
if (typeof candidate.accountId === "string" && candidate.accountId.length > 0) normalized.accountId = candidate.accountId;
|
|
552
|
+
const workbuddy = normalizeWorkBuddyMetadata(candidate.workbuddy);
|
|
553
|
+
if (workbuddy) normalized.workbuddy = workbuddy;
|
|
550
554
|
const identity = normalizeAnthropicIdentity(candidate.anthropicIdentity, candidate.access);
|
|
551
555
|
if (identity) normalized.anthropicIdentity = identity;
|
|
552
556
|
if (candidate.plan !== undefined) normalized.plan = normalizeOAuthPlan(candidate.plan) ?? null;
|
|
@@ -1152,7 +1156,7 @@ export async function upsertCredentialByIdentity(
|
|
|
1152
1156
|
return Boolean(
|
|
1153
1157
|
safe.email
|
|
1154
1158
|
&& account.credential.email
|
|
1155
|
-
&& account.credential.email.toLowerCase() === safe.email.toLowerCase(),
|
|
1159
|
+
&& account.credential.email.trim().toLowerCase() === safe.email.trim().toLowerCase(),
|
|
1156
1160
|
);
|
|
1157
1161
|
};
|
|
1158
1162
|
const existing = safe.accountId
|
|
@@ -215,6 +215,13 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
|
|
|
215
215
|
if (!record || record.deletedAt != null) continue;
|
|
216
216
|
const cred = record.credential;
|
|
217
217
|
if (!cred) continue;
|
|
218
|
+
// A grant the token endpoint already called revoked or expired cannot be refreshed; posting
|
|
219
|
+
// it again only buys another 401 per backoff window (#7592). The OAuth branch above skips
|
|
220
|
+
// needsReauth for the same reason. A re-login or a completed validation rewrites the record
|
|
221
|
+
// without the marker, which is what readmits the account. A source-linked credential spends
|
|
222
|
+
// no grant of its own, so it keeps the sweep, matching the quota probe's hold.
|
|
223
|
+
if (record.lastCodexValidationTerminal === true && record.lastCodexValidationStatus === "failed"
|
|
224
|
+
&& !cred.sourceAuthPath) continue;
|
|
218
225
|
const needsRefresh = cred.expiresAt <= nowMs + horizonMs;
|
|
219
226
|
const needsWarmup = opts.codexWarmupEnabled
|
|
220
227
|
&& !record.codexValidationPending
|
package/src/oauth/types.ts
CHANGED
|
@@ -54,7 +54,16 @@ export interface MuseOAuthMetadata {
|
|
|
54
54
|
tierName?: string;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export interface WorkBuddyOAuthMetadata {
|
|
58
|
+
uid: string;
|
|
59
|
+
domain: string;
|
|
60
|
+
enterpriseId?: string;
|
|
61
|
+
refreshExpiresAt?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
export type OAuthCredentials = {
|
|
65
|
+
/** Account-scoped WorkBuddy identity, kept inside the protected credential store. */
|
|
66
|
+
workbuddy?: WorkBuddyOAuthMetadata;
|
|
58
67
|
refresh: string;
|
|
59
68
|
access: string;
|
|
60
69
|
/** Epoch ms after any small provider-specific early-refresh margin; the shared gate adds 1 minute. */
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
4
|
+
import { WORKBUDDY_ORIGIN, WORKBUDDY_USER_AGENT, workBuddyHeaderValue, workBuddyHeaders } from "../providers/workbuddy";
|
|
5
|
+
import type { OAuthController, OAuthCredentials, WorkBuddyOAuthMetadata } from "./types";
|
|
6
|
+
|
|
7
|
+
type Json = Record<string, unknown>;
|
|
8
|
+
function record(value: unknown): Json {
|
|
9
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Json : {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class WorkBuddyApiError extends Error {
|
|
13
|
+
constructor(readonly status: number, readonly code: number) {
|
|
14
|
+
super(`WorkBuddy AI request failed (HTTP ${status}, code ${code})`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Fixed-origin, bounded reads; upstream error/profile text never enters diagnostics. */
|
|
19
|
+
export async function workBuddyApi(
|
|
20
|
+
path: string, headers: Record<string, string>, signal?: AbortSignal, post = false, payload: Json = {},
|
|
21
|
+
): Promise<Json> {
|
|
22
|
+
const timeout = AbortSignal.timeout(15_000);
|
|
23
|
+
const boundedSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
24
|
+
const response = await fetch(`${WORKBUDDY_ORIGIN}${path}`, {
|
|
25
|
+
method: post ? "POST" : "GET", redirect: "error", signal: boundedSignal,
|
|
26
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": WORKBUDDY_USER_AGENT, ...headers },
|
|
27
|
+
...(post ? { body: JSON.stringify(payload) } : {}),
|
|
28
|
+
}).catch(() => {
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
throw new Error("WorkBuddy AI request unavailable");
|
|
31
|
+
});
|
|
32
|
+
const body = await readBoundedResponseBody(response, {
|
|
33
|
+
signal: boundedSignal, maxBytes: 256 * 1024, totalTimeoutMs: 15_000, inactivityTimeoutMs: 15_000, fatalUtf8: true,
|
|
34
|
+
}).catch(() => {
|
|
35
|
+
signal?.throwIfAborted();
|
|
36
|
+
throw new Error("WorkBuddy AI response unavailable");
|
|
37
|
+
});
|
|
38
|
+
let env: Json;
|
|
39
|
+
try {
|
|
40
|
+
if (body.oversized || body.truncated || !body.displaySafe) throw new Error();
|
|
41
|
+
env = record(JSON.parse(body.text));
|
|
42
|
+
} catch {
|
|
43
|
+
signal?.throwIfAborted();
|
|
44
|
+
if (!response.ok) throw new WorkBuddyApiError(response.status, -1);
|
|
45
|
+
throw new Error("WorkBuddy AI returned an invalid response");
|
|
46
|
+
}
|
|
47
|
+
const code = typeof env.code === "number" || typeof env.code === "string" && /^\d+$/.test(env.code)
|
|
48
|
+
? Number(env.code) : NaN;
|
|
49
|
+
if (!response.ok || !Number.isFinite(code) || code !== 0) {
|
|
50
|
+
throw new WorkBuddyApiError(response.status, Number.isSafeInteger(code) ? code : -1);
|
|
51
|
+
}
|
|
52
|
+
return record(env.data);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function expiry(got: Json, absolute: string, relative: string, fallback?: number): number | undefined {
|
|
56
|
+
const at = got[absolute], seconds = got[relative];
|
|
57
|
+
if (typeof at === "number" && Number.isSafeInteger(at) && at > 0) return at;
|
|
58
|
+
if (typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0) {
|
|
59
|
+
const result = Date.now() + seconds * 1000;
|
|
60
|
+
if (Number.isSafeInteger(result)) return result;
|
|
61
|
+
}
|
|
62
|
+
return fallback;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function credentials(got: Json, metadata: WorkBuddyOAuthMetadata, previous?: OAuthCredentials): OAuthCredentials {
|
|
66
|
+
const access = got.accessToken;
|
|
67
|
+
const refresh = got.refreshToken ?? previous?.refresh;
|
|
68
|
+
const expires = expiry(got, "expiresAt", "expiresIn");
|
|
69
|
+
const domain = got.domain ?? metadata.domain;
|
|
70
|
+
if (!workBuddyHeaderValue(access, 32768) || !workBuddyHeaderValue(refresh, 32768)
|
|
71
|
+
|| !expires || expires <= Date.now() || !workBuddyHeaderValue(domain)) throw new Error("WorkBuddy AI returned invalid credentials");
|
|
72
|
+
return {
|
|
73
|
+
access, refresh, expires, source: "oauth",
|
|
74
|
+
// A uid may belong to both a personal plan and a team; they are separate account slots.
|
|
75
|
+
accountId: JSON.stringify([metadata.uid, metadata.enterpriseId ?? ""]),
|
|
76
|
+
workbuddy: { ...metadata, domain,
|
|
77
|
+
refreshExpiresAt: expiry(got, "refreshExpiresAt", "refreshExpiresIn", metadata.refreshExpiresAt) },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function poll(path: string, headers: Record<string, string>, retryCode: number, signal: AbortSignal): Promise<Json> {
|
|
82
|
+
while (true) {
|
|
83
|
+
signal.throwIfAborted();
|
|
84
|
+
try {
|
|
85
|
+
const result = await workBuddyApi(path, headers, signal);
|
|
86
|
+
if (Object.keys(result).length) return result;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!(error instanceof WorkBuddyApiError) || (error.code !== retryCode && ![408, 429].includes(error.status))) throw error;
|
|
89
|
+
}
|
|
90
|
+
await delay(1000, undefined, { signal });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function loginWorkBuddy(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
95
|
+
const deadline = AbortSignal.timeout(5 * 60_000);
|
|
96
|
+
const signal = ctrl.signal ? AbortSignal.any([ctrl.signal, deadline]) : deadline;
|
|
97
|
+
signal.throwIfAborted();
|
|
98
|
+
const state = await workBuddyApi("/v2/plugin/auth/state?platform=workbuddy-ai", {
|
|
99
|
+
"X-No-Authorization": "true", "X-No-User-Id": "true",
|
|
100
|
+
"X-No-Enterprise-Id": "true", "X-No-Department-Info": "true",
|
|
101
|
+
}, signal, true);
|
|
102
|
+
if (!workBuddyHeaderValue(state.state, 4096) || typeof state.authUrl !== "string" || state.authUrl.length > 8192) throw new Error("WorkBuddy AI returned an invalid login state");
|
|
103
|
+
let url: URL;
|
|
104
|
+
try { url = new URL(state.authUrl); } catch { throw new Error("WorkBuddy AI returned an invalid login URL"); }
|
|
105
|
+
if (![WORKBUDDY_ORIGIN, "https://www.codebuddy.ai"].includes(url.origin) || url.username || url.password) throw new Error("WorkBuddy AI returned an untrusted login URL");
|
|
106
|
+
url.searchParams.set("version", "2.0.0");
|
|
107
|
+
url.searchParams.set("loginSessionId", randomUUID());
|
|
108
|
+
ctrl.onAuth?.({ url: url.href, instructions: "Sign in to WorkBuddy AI in your browser. OpenCodex will finish automatically." });
|
|
109
|
+
const query = encodeURIComponent(state.state);
|
|
110
|
+
const token = await poll(`/v2/plugin/auth/token?state=${query}`, {}, 11217, signal);
|
|
111
|
+
if (!workBuddyHeaderValue(token.accessToken, 32768)) throw new Error("WorkBuddy AI returned no access token");
|
|
112
|
+
const domain = token.domain ?? "www.workbuddy.ai";
|
|
113
|
+
if (!workBuddyHeaderValue(domain)) throw new Error("WorkBuddy AI returned an invalid account domain");
|
|
114
|
+
const headers = { Authorization: `Bearer ${token.accessToken}`, "X-Domain": domain, "X-No-User-Id": "true", "X-No-Enterprise-Id": "true" };
|
|
115
|
+
const who = await poll(`/v2/plugin/login/account?state=${query}`, headers, 12151, signal);
|
|
116
|
+
if (!workBuddyHeaderValue(who.uid)) throw new Error("WorkBuddy AI account identity unavailable");
|
|
117
|
+
let enterpriseId = who.enterpriseId;
|
|
118
|
+
if (!enterpriseId && typeof who.type === "string" && who.type.toLowerCase() !== "personal") {
|
|
119
|
+
const result = await workBuddyApi("/v2/plugin/accounts", headers, signal);
|
|
120
|
+
const mine = (Array.isArray(result.accounts) ? result.accounts : []).map(record)
|
|
121
|
+
.filter(a => a.uid === who.uid && workBuddyHeaderValue(a.enterpriseId));
|
|
122
|
+
const same = mine.filter(a => a.type === who.type), last = mine.filter(a => a.lastLogin === true);
|
|
123
|
+
enterpriseId = (same.length === 1 ? same[0] : last.length === 1 ? last[0] : mine.length === 1 ? mine[0] : undefined)?.enterpriseId;
|
|
124
|
+
if (!enterpriseId) throw new Error("WorkBuddy AI team identity unavailable; sign in again");
|
|
125
|
+
}
|
|
126
|
+
if (enterpriseId !== undefined && enterpriseId !== "" && !workBuddyHeaderValue(enterpriseId)) throw new Error("WorkBuddy AI returned an invalid team identity");
|
|
127
|
+
return credentials(token, { uid: who.uid, domain, ...(enterpriseId ? { enterpriseId: enterpriseId as string } : {}) });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function refreshWorkBuddyToken(refresh: string, signal?: AbortSignal, previous?: OAuthCredentials): Promise<OAuthCredentials> {
|
|
131
|
+
const metadata = previous?.workbuddy;
|
|
132
|
+
if (!metadata || !workBuddyHeaderValue(refresh, 32768)) throw new Error("WorkBuddy AI requires a new browser login");
|
|
133
|
+
if (metadata.refreshExpiresAt && metadata.refreshExpiresAt <= Date.now()) throw new Error("WorkBuddy AI refresh token expired; sign in again");
|
|
134
|
+
const headers = workBuddyHeaders(undefined, metadata);
|
|
135
|
+
headers["X-Refresh-Token"] = refresh;
|
|
136
|
+
headers["X-Auth-Refresh-Source"] = "plugin";
|
|
137
|
+
const token = await workBuddyApi("/v2/plugin/auth/token/refresh", headers, signal, true);
|
|
138
|
+
return credentials(token, metadata, previous);
|
|
139
|
+
}
|
|
@@ -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
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { effectiveGoogleMode, getProviderRegistryEntry } from "./registry";
|
|
3
|
+
import type { OAuthCredentials } from "../oauth/types";
|
|
4
|
+
import type { OcxProviderConfig } from "../types";
|
|
5
|
+
|
|
6
|
+
const ARGON_TOKEN = /(?:^|[^a-z0-9])argon(?:[^a-z0-9]|$)/i;
|
|
7
|
+
/** Argon has an independent quota pool, including its native effort-suffixed identities. */
|
|
8
|
+
export function isAntigravityArgonIdentity(identity: string): boolean { return ARGON_TOKEN.test(identity); }
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The OAuth store key whose account both inference and the quota probe resolve. Inference reads
|
|
12
|
+
* `store[route.providerName]` and refreshes through the OAuth definition registered under that same
|
|
13
|
+
* name, and `ocx login` only accepts registered names, so a renamed provider row never authenticates
|
|
14
|
+
* as this account. Evidence is bound to this key alone; a renamed row reads unknown.
|
|
15
|
+
*/
|
|
16
|
+
export const ANTIGRAVITY_OAUTH_STORE_KEY = "google-antigravity";
|
|
17
|
+
|
|
18
|
+
/** Accounting and inference must refer to the same OAuth store entry and the supported Antigravity destination. */
|
|
19
|
+
export function antigravityQuotaEligible(name: string, provider: OcxProviderConfig): boolean {
|
|
20
|
+
if (name !== ANTIGRAVITY_OAUTH_STORE_KEY || provider.disabled || provider.authMode !== "oauth"
|
|
21
|
+
|| effectiveGoogleMode(name, provider) !== "cloud-code-assist") return false;
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(provider.baseUrl);
|
|
24
|
+
const expected = new URL(getProviderRegistryEntry(ANTIGRAVITY_OAUTH_STORE_KEY)!.baseUrl);
|
|
25
|
+
return url.href.replace(/\/$/, "") === expected.href.replace(/\/$/, "");
|
|
26
|
+
} catch { return false; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Private generation includes the accounting project, not just the rotating bearer. */
|
|
30
|
+
export function antigravityQuotaCredentialGeneration(credential: OAuthCredentials): string {
|
|
31
|
+
return createHash("sha256").update(JSON.stringify([credential.refresh, credential.access, credential.expires, credential.projectId])).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Immutable instance/destination policy, never returned to decision services. */
|
|
35
|
+
export function antigravityQuotaPolicy(name: string, provider: OcxProviderConfig): string {
|
|
36
|
+
return createHash("sha256").update(JSON.stringify([name, provider])).digest("hex");
|
|
37
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Qwen Cloud OpenAI-compatible endpoint presets.
|
|
3
3
|
* Token plan is the GUI default; pay-as-you-go and custom are selectable.
|
|
4
|
+
* Token plan host per https://docs.qwencloud.com/token-plan/personal/token-plan-personal-quickstart
|
|
5
|
+
* (checked 2026-10-23); a saved older host stays usable as a custom URL.
|
|
4
6
|
*/
|
|
5
7
|
export const QWEN_CLOUD_TOKEN_PLAN_BASE_URL =
|
|
6
|
-
"https://token-plan.
|
|
8
|
+
"https://token-plan.maas.qwencloudapi.com/compatible-mode/v1";
|
|
7
9
|
export const QWEN_CLOUD_PAYG_BASE_URL =
|
|
8
10
|
"https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
9
11
|
|
|
@@ -23,10 +25,11 @@ export const QWEN_CLOUD_BASE_URL_CHOICES: readonly ProviderBaseUrlChoice[] = [
|
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Alibaba Token Plan International (ap-southeast-1) endpoint presets.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* Separate regional account and billing destination; both regions offer both tiers.
|
|
29
|
+
* Alibaba Cloud's own quick start still documents this host (checked 2026-10-23):
|
|
30
|
+
* https://www.alibabacloud.com/help/en/model-studio/token-plan-personal-quick-start
|
|
31
|
+
* Qwen Cloud documents a different Token Plan host, so the two constants are
|
|
32
|
+
* independent and must not be assumed equal.
|
|
30
33
|
*/
|
|
31
34
|
export const ALIBABA_INTL_TOKEN_PLAN_BASE_URL =
|
|
32
35
|
"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1";
|