@bitkyc08/opencodex 2.68.0 → 2.69.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 +25 -17
- package/gui/dist/assets/App-DDbgOTh6.js +51 -0
- package/gui/dist/assets/{Tray-Bn8WhKBH.js → Tray-uqKRqfvw.js} +1 -1
- package/gui/dist/assets/index-BF9QnKO_.css +1 -0
- package/gui/dist/assets/index-CuaBJkG4.js +86 -0
- package/gui/dist/assets/{quota-summary-DL-bNuNL.js → quota-summary-COppQSVq.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/factory-droid.svg +8 -0
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +109 -11
- package/src/adapters/coding-agent/protocol.ts +106 -11
- package/src/adapters/coding-agent/turn.ts +23 -18
- package/src/adapters/google-errors.ts +16 -1
- package/src/adapters/google-http.ts +14 -0
- package/src/adapters/openai-chat/passthrough.ts +2 -0
- package/src/adapters/openai-chat/summary-budget.ts +72 -0
- package/src/adapters/openai-chat/tool-call-id-remint.ts +11 -2
- package/src/adapters/openai-chat.ts +6 -9
- package/src/bridge/response-json.ts +3 -1
- package/src/bridge/sse.ts +3 -1
- package/src/claude/desktop-picker.ts +6 -2
- package/src/claude/desktop-system-proxy.ts +155 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/picker-ca-cleanup.ts +31 -0
- package/src/claude/intercept/picker-ca.ts +283 -83
- package/src/claude/intercept/picker-models.ts +4 -1
- package/src/claude/intercept/picker-trust.ts +15 -2
- package/src/claude/intercept/runtime.ts +99 -32
- package/src/cli/account-api.ts +5 -2
- package/src/cli/account-extended.ts +47 -11
- package/src/cli/account-target.ts +12 -7
- package/src/cli/account.ts +47 -3
- package/src/cli/capabilities.ts +33 -7
- package/src/cli/claude-agent-startup-sync.ts +2 -0
- package/src/cli/claude-desktop.ts +37 -13
- package/src/cli/cross-home-owner.ts +138 -0
- package/src/cli/dispatch.ts +1 -1
- package/src/cli/doctor.ts +18 -8
- package/src/cli/ensure-desired-integrations.ts +5 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +15 -18
- package/src/cli/integrations.ts +8 -1
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +106 -4
- package/src/cli/models.ts +6 -1
- package/src/cli/opencode.ts +2 -83
- package/src/cli/registry.ts +3 -2
- package/src/cli/status.ts +86 -7
- package/src/client/connect.ts +24 -10
- package/src/client/link-ingress.ts +3 -1
- package/src/client/link-join.ts +76 -9
- package/src/client/link-relay-transport.ts +184 -0
- package/src/client/link-relay.ts +16 -4
- package/src/client/machine-listener.ts +2 -0
- package/src/clients/config-export/constants.ts +8 -0
- package/src/clients/config-export/contracts.ts +13 -1
- package/src/clients/config-export/droid.ts +71 -0
- package/src/clients/config-export/kilo.ts +116 -0
- package/src/clients/config-export.ts +33 -2
- package/src/codex/auth-api/routes.ts +1 -1
- package/src/codex/catalog/access-programs.ts +14 -2
- package/src/codex/catalog/provider-models.ts +1 -1
- package/src/codex/catalog-auto-refresh.ts +122 -0
- package/src/codex/config-drift-heal.ts +80 -0
- package/src/codex/desktop-switches.ts +7 -5
- package/src/codex/inject/bounded-config-reader.ts +69 -0
- package/src/codex/inject/config-toml.ts +31 -3
- package/src/codex/inject/routing-target.ts +6 -1
- package/src/codex/inject.ts +4 -0
- package/src/codex/model-entitlements.ts +32 -5
- package/src/codex/native-profile-startup.ts +17 -5
- package/src/codex/project-config-warnings.ts +72 -21
- package/src/codex/quota-auto-refresh-state.ts +8 -2
- package/src/codex/quota-auto-refresh.ts +80 -9
- package/src/codex/sync.ts +10 -0
- package/src/combos/failover.ts +4 -1
- package/src/combos/jev.ts +15 -4
- package/src/combos/types.ts +16 -0
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +29 -0
- package/src/config/macos-system-proxy.ts +114 -0
- package/src/config/proxy-env.ts +67 -20
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +23 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +220 -144
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +12 -3
- package/src/integrations/droid-settings.ts +65 -0
- package/src/integrations/kilo-candidates.ts +30 -0
- package/src/integrations/mutation-plan.ts +62 -7
- package/src/integrations/native/ownership-preflight.ts +46 -12
- package/src/integrations/registry.ts +136 -7
- package/src/integrations/state.ts +52 -4
- package/src/integrations/target.ts +3 -0
- package/src/integrations/writer.ts +55 -14
- package/src/lib/account-selection-events.ts +14 -0
- package/src/lib/jsonc.ts +110 -0
- package/src/lib/standalone.ts +9 -1
- package/src/lib/windows-system-proxy.ts +101 -1
- package/src/link/relay-auth.ts +29 -0
- package/src/link/ssh-argv.ts +6 -3
- package/src/link/ssh-runner.ts +5 -5
- package/src/oauth/generic-account-failover.ts +52 -8
- package/src/oauth/index.ts +37 -14
- package/src/oauth/store.ts +96 -11
- package/src/oauth/token-guardian.ts +7 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +5 -3
- package/src/providers/fastwire.ts +3 -0
- package/src/providers/key-failover.ts +13 -5
- package/src/providers/kiro-model-catalog.ts +4 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-tiers-destination.ts +9 -0
- package/src/providers/quota/account-cache.ts +17 -3
- package/src/providers/quota/vendor-probes-oauth.ts +2 -0
- package/src/providers/quota.ts +2 -2
- package/src/responses/code-mode-helper-compat.ts +14 -1
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/images.ts +70 -20
- package/src/server/index/link-listener.ts +20 -3
- package/src/server/index/link-relay-sessions.ts +138 -0
- package/src/server/index/optional-listeners.ts +11 -1
- package/src/server/index/serve-options.ts +12 -0
- package/src/server/inference/client-encoder-delivery.ts +1 -0
- package/src/server/local-management-read-client.ts +21 -2
- package/src/server/management/agent-settings-routes.ts +19 -5
- package/src/server/management/combo-routes.ts +5 -3
- package/src/server/management/config-routes.ts +32 -6
- package/src/server/management/model-routes.ts +232 -7
- package/src/server/management/model-rows.ts +199 -1
- package/src/server/management/oauth-account-routes.ts +63 -3
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/port-reclaim.ts +170 -32
- package/src/server/responses/adapter-delivery.ts +6 -4
- package/src/server/responses/adapter-dispatch.ts +72 -2
- package/src/server/responses/core-combo.ts +6 -1
- package/src/server/responses/core-normalize.ts +19 -4
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/memory-models.ts +169 -0
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/passthrough-dispatch.ts +10 -0
- package/src/server/responses/request-prepare.ts +86 -4
- package/src/server/responses/request-transport.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +5 -2
- package/src/server/responses/shadow-target-availability.ts +17 -4
- package/src/server/responses/sidecar-execution.ts +18 -2
- package/src/server/responses/skills-snapshot.ts +239 -0
- package/src/server/responses-undeclared-tool-guard.ts +43 -6
- package/src/server/search.ts +1 -1
- package/src/service/guards.ts +4 -5
- package/src/service/state.ts +39 -2
- package/src/service.ts +2 -2
- package/src/types/config.ts +46 -4
- package/src/types/provider.ts +9 -0
- package/src/types/request.ts +6 -0
- package/src/types/tools.ts +38 -8
- package/src/types.ts +3 -0
- package/src/update/async-check.ts +5 -1
- package/src/update/index.ts +30 -12
- package/src/update/job.ts +3 -1
- package/src/update/pnpm-read-policy.d.mts +4 -0
- package/src/update/pnpm-read-policy.mjs +44 -0
- package/src/update/worker-launch.ts +152 -11
- package/src/usage/cost.ts +6 -3
- package/src/usage/log.ts +4 -0
- package/src/web-search/backends.ts +2 -2
- package/src/web-search/devin-executor.ts +1 -1
- package/src/web-search/sidecar-providers.ts +5 -5
- package/gui/dist/assets/App-mpsibsFj.js +0 -51
- package/gui/dist/assets/index-BDUBS8PW.css +0 -1
- package/gui/dist/assets/index-kUE8tuAK.js +0 -86
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { Agent, request, type IncomingMessage } from "node:http";
|
|
2
|
+
import type { Socket } from "node:net";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { createBrotliDecompress, createGunzip, createInflate } from "node:zlib";
|
|
5
|
+
import { serviceApiTokenFingerprint } from "../lib/service-secrets";
|
|
6
|
+
import { clearableDeadline } from "../lib/abort";
|
|
7
|
+
import {
|
|
8
|
+
LINK_RELAY_AUTH_PATH, LINK_RELAY_AUTH_TIMEOUT_MS, LINK_RELAY_AUTH_VERSION,
|
|
9
|
+
LINK_RELAY_SESSION_HEADER, linkRelayChallenge, linkRelayProofMatches,
|
|
10
|
+
} from "../link/relay-auth";
|
|
11
|
+
|
|
12
|
+
export interface BoundLinkRelayTarget {
|
|
13
|
+
tunnelPort: number;
|
|
14
|
+
admissionKey: string;
|
|
15
|
+
linkId?: string;
|
|
16
|
+
apiKeyId?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class LinkRelayAuthenticationError extends Error {
|
|
20
|
+
constructor(readonly unrecognized = false) {
|
|
21
|
+
super(unrecognized ? "link relay authentication unavailable; upgrade the Home and Child or re-link"
|
|
22
|
+
: "link relay authentication failed");
|
|
23
|
+
this.name = "LinkRelayAuthenticationError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One physical connection, ever. A closed keep-alive socket cannot trigger a silent reconnect. */
|
|
28
|
+
class SingleConnectionAgent extends Agent {
|
|
29
|
+
socket: Socket | undefined;
|
|
30
|
+
private opened = false;
|
|
31
|
+
private closeFlight?: Promise<void>;
|
|
32
|
+
/** Drain the socket close event before the request reports a terminal outcome. */
|
|
33
|
+
close(): Promise<void> {
|
|
34
|
+
if (this.closeFlight) return this.closeFlight;
|
|
35
|
+
const socket = this.socket;
|
|
36
|
+
this.closeFlight = !socket || socket.closed ? Promise.resolve()
|
|
37
|
+
: new Promise<void>(resolve => { socket.once("close", () => resolve()); });
|
|
38
|
+
this.destroy();
|
|
39
|
+
return this.closeFlight;
|
|
40
|
+
}
|
|
41
|
+
override createConnection(...[options, callback]: Parameters<Agent["createConnection"]>): ReturnType<Agent["createConnection"]> {
|
|
42
|
+
if (this.opened) {
|
|
43
|
+
if (!callback) throw new LinkRelayAuthenticationError();
|
|
44
|
+
callback(new LinkRelayAuthenticationError(), this.socket!);
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
this.opened = true;
|
|
48
|
+
this.socket = super.createConnection(options, callback) as Socket;
|
|
49
|
+
return this.socket;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function requestOnConnection(
|
|
54
|
+
url: URL, init: RequestInit, agent: SingleConnectionAgent, authenticatedSocket?: Socket,
|
|
55
|
+
): Promise<IncomingMessage> {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const headers = Object.fromEntries(new Headers(init.headers));
|
|
58
|
+
// No implicit decompression negotiation; handle an explicitly encoded response below anyway.
|
|
59
|
+
headers["accept-encoding"] = "identity";
|
|
60
|
+
const outgoing = request(url, {
|
|
61
|
+
method: init.method ?? "GET", headers, agent, signal: init.signal ?? undefined,
|
|
62
|
+
maxHeaderSize: 64 * 1024,
|
|
63
|
+
}, resolve);
|
|
64
|
+
outgoing.on("error", reject);
|
|
65
|
+
outgoing.on("upgrade", (_response, socket) => { socket.destroy(); reject(new LinkRelayAuthenticationError()); });
|
|
66
|
+
outgoing.on("socket", socket => {
|
|
67
|
+
if (authenticatedSocket && socket !== authenticatedSocket) outgoing.destroy(new LinkRelayAuthenticationError());
|
|
68
|
+
});
|
|
69
|
+
void (async () => {
|
|
70
|
+
const body = init.body;
|
|
71
|
+
if (body instanceof ReadableStream) {
|
|
72
|
+
const reader = body.getReader();
|
|
73
|
+
const abort = () => { void reader.cancel(init.signal?.reason).catch(() => {}); };
|
|
74
|
+
init.signal?.addEventListener("abort", abort, { once: true });
|
|
75
|
+
outgoing.once("close", abort);
|
|
76
|
+
try {
|
|
77
|
+
for (;;) {
|
|
78
|
+
init.signal?.throwIfAborted();
|
|
79
|
+
const next = await reader.read();
|
|
80
|
+
if (next.done) break;
|
|
81
|
+
// Await the write callback: upstream backpressure never buffers the whole upload.
|
|
82
|
+
await new Promise<void>((done, fail) => outgoing.write(next.value,
|
|
83
|
+
error => error ? fail(error) : done()));
|
|
84
|
+
}
|
|
85
|
+
} finally {
|
|
86
|
+
init.signal?.removeEventListener("abort", abort);
|
|
87
|
+
outgoing.removeListener("close", abort);
|
|
88
|
+
try { await reader.cancel(); } catch { /* preserve the upload outcome */ }
|
|
89
|
+
reader.releaseLock();
|
|
90
|
+
}
|
|
91
|
+
} else if (body != null) {
|
|
92
|
+
if (typeof body === "string" || body instanceof Uint8Array) outgoing.write(body);
|
|
93
|
+
else throw new TypeError("unsupported link relay body");
|
|
94
|
+
}
|
|
95
|
+
outgoing.end();
|
|
96
|
+
})().catch(error => { outgoing.destroy(error instanceof Error ? error : new Error("link upload failed")); reject(error); });
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function responseHeaders(incoming: IncomingMessage): Headers {
|
|
101
|
+
const headers = new Headers();
|
|
102
|
+
for (let i = 0; i < incoming.rawHeaders.length; i += 2) {
|
|
103
|
+
headers.append(incoming.rawHeaders[i]!, incoming.rawHeaders[i + 1]!);
|
|
104
|
+
}
|
|
105
|
+
return headers;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Node HTTP compatibility is confined here; the relay continues to expose a Web Response/stream. */
|
|
109
|
+
async function responseOnConnection(incoming: IncomingMessage, agent: SingleConnectionAgent, method: string): Promise<Response> {
|
|
110
|
+
const headers = responseHeaders(incoming);
|
|
111
|
+
const status = incoming.statusCode ?? 502;
|
|
112
|
+
if (method === "HEAD" || status === 204 || status === 205 || status === 304) {
|
|
113
|
+
for await (const _chunk of incoming) { /* drain a header-only response */ }
|
|
114
|
+
await agent.close();
|
|
115
|
+
return new Response(null, { status, headers });
|
|
116
|
+
}
|
|
117
|
+
let source: Readable = incoming;
|
|
118
|
+
const encoding = headers.get("content-encoding")?.trim().toLowerCase();
|
|
119
|
+
const decoder = encoding === "gzip" ? createGunzip() : encoding === "deflate" ? createInflate()
|
|
120
|
+
: encoding === "br" ? createBrotliDecompress() : undefined;
|
|
121
|
+
if (encoding && encoding !== "identity" && !decoder) {
|
|
122
|
+
throw new Error("unsupported link response encoding");
|
|
123
|
+
}
|
|
124
|
+
if (decoder) {
|
|
125
|
+
incoming.on("error", error => decoder.destroy(error));
|
|
126
|
+
source = incoming.pipe(decoder);
|
|
127
|
+
}
|
|
128
|
+
const reader = (Readable.toWeb(source) as unknown as ReadableStream<Uint8Array>).getReader();
|
|
129
|
+
let finished = false;
|
|
130
|
+
const finish = async () => {
|
|
131
|
+
if (finished) return agent.close();
|
|
132
|
+
finished = true;
|
|
133
|
+
if (source !== incoming) source.destroy();
|
|
134
|
+
await agent.close();
|
|
135
|
+
try { reader.releaseLock(); } catch { /* a cancelled read may still be settling */ }
|
|
136
|
+
};
|
|
137
|
+
return new Response(new ReadableStream<Uint8Array>({
|
|
138
|
+
async pull(controller) {
|
|
139
|
+
try {
|
|
140
|
+
const next = await reader.read();
|
|
141
|
+
if (next.done) { await finish(); controller.close(); }
|
|
142
|
+
else controller.enqueue(next.value);
|
|
143
|
+
} catch (error) { await finish(); controller.error(error); }
|
|
144
|
+
},
|
|
145
|
+
async cancel(reason) {
|
|
146
|
+
try { await reader.cancel(reason); } finally { await finish(); }
|
|
147
|
+
},
|
|
148
|
+
}, { highWaterMark: 0 }), { status, headers });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Authenticate and stream one request on the SAME socket; never fall back to an ordinary fetch. */
|
|
152
|
+
export async function fetchBoundLinkRelay(target: BoundLinkRelayTarget, destination: string, init: RequestInit): Promise<Response> {
|
|
153
|
+
if (!target.linkId || !target.apiKeyId) throw new LinkRelayAuthenticationError();
|
|
154
|
+
const url = new URL(destination);
|
|
155
|
+
if (url.origin !== `http://127.0.0.1:${target.tunnelPort}`) throw new LinkRelayAuthenticationError();
|
|
156
|
+
const challenge = linkRelayChallenge(serviceApiTokenFingerprint(target.admissionKey), target.apiKeyId, target.linkId);
|
|
157
|
+
const probe = new URL(LINK_RELAY_AUTH_PATH, url);
|
|
158
|
+
probe.search = new URLSearchParams({ version: LINK_RELAY_AUTH_VERSION, key: target.apiKeyId,
|
|
159
|
+
link: target.linkId, nonce: challenge.nonce, proof: challenge.caller }).toString();
|
|
160
|
+
const agent = new SingleConnectionAgent({ keepAlive: true, maxSockets: 1, maxFreeSockets: 1 });
|
|
161
|
+
const deadline = clearableDeadline(LINK_RELAY_AUTH_TIMEOUT_MS, init.signal ?? undefined);
|
|
162
|
+
try {
|
|
163
|
+
const response = await requestOnConnection(probe, { method: "GET", signal: deadline.signal }, agent);
|
|
164
|
+
const socket = agent.socket;
|
|
165
|
+
const proof = response.headers["x-opencodex-link-proof"];
|
|
166
|
+
if (response.statusCode !== 204 || typeof proof !== "string" || !linkRelayProofMatches(proof, challenge.expected)) {
|
|
167
|
+
throw new LinkRelayAuthenticationError(response.statusCode === 404);
|
|
168
|
+
}
|
|
169
|
+
// Drain the credential-free response before the Agent can reuse its socket.
|
|
170
|
+
for await (const chunk of response) {
|
|
171
|
+
if (chunk.length) throw new LinkRelayAuthenticationError();
|
|
172
|
+
}
|
|
173
|
+
deadline.clear();
|
|
174
|
+
init.signal?.throwIfAborted();
|
|
175
|
+
if (!socket || socket.destroyed || socket !== agent.socket) throw new LinkRelayAuthenticationError();
|
|
176
|
+
const headers = new Headers(init.headers);
|
|
177
|
+
headers.set(LINK_RELAY_SESSION_HEADER, challenge.nonce);
|
|
178
|
+
const incoming = await requestOnConnection(url, { ...init, headers }, agent, socket);
|
|
179
|
+
return await responseOnConnection(incoming, agent, init.method ?? "GET");
|
|
180
|
+
} catch (error) {
|
|
181
|
+
await agent.close();
|
|
182
|
+
throw error;
|
|
183
|
+
} finally { deadline.clear(); }
|
|
184
|
+
}
|
package/src/client/link-relay.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fetchBoundLinkRelay, LinkRelayAuthenticationError } from "./link-relay-transport";
|
|
1
2
|
import {
|
|
2
3
|
boundedRelayResponseStream,
|
|
3
4
|
filterRelayHeaders,
|
|
@@ -10,6 +11,8 @@ import { resolveInboundBodyLimitBytes } from "../server/request-decompress";
|
|
|
10
11
|
|
|
11
12
|
export interface LinkRelayTarget {
|
|
12
13
|
tunnelPort: number;
|
|
14
|
+
linkId?: string;
|
|
15
|
+
apiKeyId?: string;
|
|
13
16
|
/**
|
|
14
17
|
* The stored link key. The relay sends it in place of every caller credential, so the Home
|
|
15
18
|
* admits the request as this link and serves it with its own accounts.
|
|
@@ -42,8 +45,8 @@ export interface LinkTunnelGate {
|
|
|
42
45
|
/**
|
|
43
46
|
* The gate for a Home-initiated link. The Home runs `ssh -R` and owns the forward, so the Child
|
|
44
47
|
* has no tunnel supervisor and no local SSH process whose socket it could prove. This gate keeps
|
|
45
|
-
* the
|
|
46
|
-
*
|
|
48
|
+
* the absence of local process ownership proof for that link only. The data transport still
|
|
49
|
+
* authenticates its physical connection; this gate never holds a reconnect. Child-initiated links keep their
|
|
47
50
|
* supervisor, and a relay with no gate at all still refuses every request.
|
|
48
51
|
*/
|
|
49
52
|
export const HOME_INITIATED_LINK_TUNNEL: LinkTunnelGate = {
|
|
@@ -53,6 +56,7 @@ export const HOME_INITIATED_LINK_TUNNEL: LinkTunnelGate = {
|
|
|
53
56
|
};
|
|
54
57
|
|
|
55
58
|
export interface LinkRelayDeps {
|
|
59
|
+
/** Trusted complete-send test seam. Production always uses the connection-bound transport. */
|
|
56
60
|
fetchImpl?: typeof fetch;
|
|
57
61
|
clock?: LinkRelayClock;
|
|
58
62
|
/** Time allowed for the Home's response headers; a caller abort still ends the wait sooner. */
|
|
@@ -93,7 +97,10 @@ const RESPONSE_OMITTED_HEADERS = new Set(["content-encoding", "content-length"])
|
|
|
93
97
|
* Caller credentials never cross the tunnel. The Child's own ChatGPT or Anthropic credential
|
|
94
98
|
* stays on the Child, and the Home sees exactly one admission: the link key.
|
|
95
99
|
*/
|
|
96
|
-
const CALLER_CREDENTIAL_HEADERS = [
|
|
100
|
+
const CALLER_CREDENTIAL_HEADERS = [
|
|
101
|
+
"authorization", "api-key", "x-api-key", "x-goog-api-key", "x-opencodex-api-key",
|
|
102
|
+
"chatgpt-account-id", "cookie",
|
|
103
|
+
] as const;
|
|
97
104
|
|
|
98
105
|
function jsonError(status: number, error: string, retry = false): Response {
|
|
99
106
|
const headers = retry ? { "Retry-After": String(LINK_RELAY_RETRY_AFTER_SECONDS) } : undefined;
|
|
@@ -376,8 +383,13 @@ export async function relayLinkDataRequest(
|
|
|
376
383
|
signal: relayAbort.signal,
|
|
377
384
|
...(body ? { body, duplex: "half" } : {}),
|
|
378
385
|
};
|
|
379
|
-
upstream = await (deps.fetchImpl
|
|
386
|
+
upstream = await (deps.fetchImpl ? deps.fetchImpl(destination, init) : fetchBoundLinkRelay(target, destination, init));
|
|
380
387
|
} catch (error) {
|
|
388
|
+
if (error instanceof LinkRelayAuthenticationError) {
|
|
389
|
+
cleanup();
|
|
390
|
+
try { await body?.cancel(); } catch { /* an upload might already be drained */ }
|
|
391
|
+
return jsonError(503, error.message, true);
|
|
392
|
+
}
|
|
381
393
|
if (bodyOverflow) {
|
|
382
394
|
cleanup();
|
|
383
395
|
return jsonError(413, "link relay request body too large");
|
|
@@ -116,6 +116,8 @@ export function startMachineListener(
|
|
|
116
116
|
const linkIngress: LinkIngress | null = linkMode
|
|
117
117
|
? {
|
|
118
118
|
tunnelPort: connection.link!.tunnelPort,
|
|
119
|
+
linkId: connection.link!.linkId,
|
|
120
|
+
apiKeyId: connection.apiKeyId,
|
|
119
121
|
policy: requestPolicyView(config, "127.0.0.1"),
|
|
120
122
|
linkKey: deps.linkKeySource ?? createLinkKeySource(connection.tokenFingerprint, deps.linkKey),
|
|
121
123
|
relay: { fetchImpl: deps.fetchImpl, bodyLimitBytes: inboundBodyLimit, tunnel: deps.linkTunnel, ...deps.linkRelay },
|
|
@@ -14,6 +14,14 @@ export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json";
|
|
|
14
14
|
*/
|
|
15
15
|
export const OPENCODE_API_KEY_ENV = "OPENCODEX_OPENCODE_API_KEY";
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Env var carrying the proxy admission key to Kilo. Independent of OpenCode's
|
|
19
|
+
* so the two clients can keep distinct credentials.
|
|
20
|
+
*/
|
|
21
|
+
export const KILO_API_KEY_ENV = "OPENCODEX_KILO_API_KEY";
|
|
22
|
+
export const KILO_API_KEY_ENV_REF = `{env:${KILO_API_KEY_ENV}}`;
|
|
23
|
+
export const KILO_CONFIG_SCHEMA = "https://app.kilo.ai/config.json";
|
|
24
|
+
|
|
17
25
|
/** Env reference shared by apiKey and the dedicated proxy admission header. */
|
|
18
26
|
export const OPENCODE_API_KEY_ENV_REF = `{env:${OPENCODE_API_KEY_ENV}}`;
|
|
19
27
|
|
|
@@ -105,7 +105,9 @@ export type ExportClientId =
|
|
|
105
105
|
| "aside"
|
|
106
106
|
| "raycast"
|
|
107
107
|
| "omo"
|
|
108
|
-
| "cline"
|
|
108
|
+
| "cline"
|
|
109
|
+
| "kilo"
|
|
110
|
+
| "droid";
|
|
109
111
|
|
|
110
112
|
export interface ExportClientSpec {
|
|
111
113
|
id: ExportClientId;
|
|
@@ -146,6 +148,16 @@ export interface ExportClientSpec {
|
|
|
146
148
|
* reasoning as the Grok managed block's non-loopback refusal.
|
|
147
149
|
*/
|
|
148
150
|
loopbackOnly: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* True when the destination file may carry comments and trailing commas
|
|
153
|
+
* even though `format` is "json" and serialization stays pretty JSON.
|
|
154
|
+
*
|
|
155
|
+
* Parse tolerates them by canonicalizing the text before the rewrite-safety
|
|
156
|
+
* scan (Kilo's kilo.jsonc). A spec flag rather than a client-name branch:
|
|
157
|
+
* the next OpenCode-family client opts in here instead of growing another
|
|
158
|
+
* `clientId ===` check at every parse site.
|
|
159
|
+
*/
|
|
160
|
+
jsonc?: boolean;
|
|
149
161
|
}
|
|
150
162
|
|
|
151
163
|
export interface PiModelEntry {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { win32, join } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { exportPresentationLabel } from "../model-presentation";
|
|
4
|
+
import type { ExportContext, ManagedContribution, OpencodeLaunchEnv } from "./contracts";
|
|
5
|
+
import { normalizeExportModels } from "./model-metadata";
|
|
6
|
+
import { formatSelectorConjunction } from "../../integrations/merge";
|
|
7
|
+
|
|
8
|
+
/** Factory personal settings: https://docs.factory.ai/model-independence/byok */
|
|
9
|
+
export interface DroidModelEntry {
|
|
10
|
+
model: string;
|
|
11
|
+
displayName: string;
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
provider: "generic-chat-completion-api";
|
|
14
|
+
noImageSupport: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DroidGeneratedConfig { customModels: DroidModelEntry[] }
|
|
18
|
+
|
|
19
|
+
const isWindowsHome = (home: string) => /^[A-Za-z]:[\\/]|^\\\\/.test(home);
|
|
20
|
+
|
|
21
|
+
export function droidHomeDir(_env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
22
|
+
return isWindowsHome(home) ? win32.join(home, ".factory") : join(home, ".factory");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function droidConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
26
|
+
const root = droidHomeDir(env, home);
|
|
27
|
+
return isWindowsHome(root) ? win32.join(root, "settings.json") : join(root, "settings.json");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildDroidRows(ctx: ExportContext): Array<{ row: DroidModelEntry; selector: string }> {
|
|
31
|
+
const rows: Array<{ row: DroidModelEntry; selector: string }> = [];
|
|
32
|
+
for (const model of normalizeExportModels(ctx.models)) {
|
|
33
|
+
const displayName = `OpenCodex: ${exportPresentationLabel(model)}`;
|
|
34
|
+
const selector = formatSelectorConjunction([
|
|
35
|
+
{ field: "model", value: model.namespaced },
|
|
36
|
+
{ field: "displayName", value: displayName },
|
|
37
|
+
]);
|
|
38
|
+
// A row we cannot address safely cannot be managed or exported.
|
|
39
|
+
if (!selector) continue;
|
|
40
|
+
rows.push({ selector, row: {
|
|
41
|
+
model: model.namespaced,
|
|
42
|
+
displayName,
|
|
43
|
+
baseUrl: ctx.baseUrl,
|
|
44
|
+
provider: "generic-chat-completion-api",
|
|
45
|
+
noImageSupport: !model.inputModalities?.includes("image"),
|
|
46
|
+
} });
|
|
47
|
+
}
|
|
48
|
+
return rows;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildDroidClientConfig(ctx: ExportContext): DroidGeneratedConfig {
|
|
52
|
+
const rows = buildDroidRows(ctx);
|
|
53
|
+
if (ctx.models.length > 0 && rows.length === 0) {
|
|
54
|
+
throw new Error("Factory Droid has no addressable models in the selected catalog");
|
|
55
|
+
}
|
|
56
|
+
return { customModels: rows.map(({ row }) => row) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function summarizeDroid(document: unknown) {
|
|
60
|
+
const rows = (document as DroidGeneratedConfig | undefined)?.customModels;
|
|
61
|
+
const count = Array.isArray(rows) ? rows.length : 0;
|
|
62
|
+
// Droid's personal schema has no context-window field.
|
|
63
|
+
return { modelCount: count, modelsWithoutLimits: count };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildDroidContribution(ctx: ExportContext): ManagedContribution {
|
|
67
|
+
return {
|
|
68
|
+
clientId: "droid",
|
|
69
|
+
fragments: buildDroidRows(ctx).map(({ row, selector }) => ({ path: ["customModels", selector], value: row })),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, win32 } from "node:path";
|
|
4
|
+
import type { ExportContext, ManagedContribution, OpencodeLaunchEnv } from "./contracts";
|
|
5
|
+
import {
|
|
6
|
+
KILO_API_KEY_ENV_REF,
|
|
7
|
+
KILO_CONFIG_SCHEMA,
|
|
8
|
+
OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG,
|
|
9
|
+
OPENCODE_PROVIDER_ID,
|
|
10
|
+
} from "./constants";
|
|
11
|
+
import {
|
|
12
|
+
authoritativeContextWindow,
|
|
13
|
+
exportModelLabel,
|
|
14
|
+
normalizeExportModels,
|
|
15
|
+
opencodeModelCapabilities,
|
|
16
|
+
outputBudgetFor,
|
|
17
|
+
proxyAdmissionHeaders,
|
|
18
|
+
singleFragment,
|
|
19
|
+
} from "./model-metadata";
|
|
20
|
+
|
|
21
|
+
export const KILO_CONFIG_CANDIDATES = [
|
|
22
|
+
"kilo.jsonc",
|
|
23
|
+
"kilo.json",
|
|
24
|
+
"opencode.jsonc",
|
|
25
|
+
"opencode.json",
|
|
26
|
+
"config.json",
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
export interface KiloModelEntry {
|
|
30
|
+
name: string;
|
|
31
|
+
limit?: { context: number; output: number };
|
|
32
|
+
attachment?: boolean;
|
|
33
|
+
modalities?: { input: string[]; output: string[] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface KiloProviderBlock {
|
|
37
|
+
npm: string;
|
|
38
|
+
name: string;
|
|
39
|
+
options: {
|
|
40
|
+
baseURL: string;
|
|
41
|
+
apiKey?: string;
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
};
|
|
44
|
+
models: Record<string, KiloModelEntry>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface KiloGeneratedConfig {
|
|
48
|
+
$schema: string;
|
|
49
|
+
provider: Record<string, KiloProviderBlock>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function windowsPath(path: string): boolean {
|
|
53
|
+
return /^[A-Za-z]:[\\/]/.test(path) || path.startsWith("\\\\");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function kiloHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
57
|
+
const override = env.XDG_CONFIG_HOME || undefined;
|
|
58
|
+
const pathJoin = windowsPath(override ?? home) ? win32.join : join;
|
|
59
|
+
const xdg = override ?? pathJoin(home, ".config");
|
|
60
|
+
return pathJoin(xdg, "kilo");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function kiloCandidatePath(dir: string, name: string): string {
|
|
64
|
+
return (windowsPath(dir) ? win32.join : join)(dir, name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function kiloConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
68
|
+
const dir = kiloHomeDir(env, home);
|
|
69
|
+
for (const name of KILO_CONFIG_CANDIDATES) {
|
|
70
|
+
const path = kiloCandidatePath(dir, name);
|
|
71
|
+
if (existsSync(path)) return path;
|
|
72
|
+
}
|
|
73
|
+
return kiloCandidatePath(dir, "kilo.jsonc");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function kiloProviderBlock(ctx: ExportContext): KiloProviderBlock {
|
|
77
|
+
const config = ctx.config ?? OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG;
|
|
78
|
+
const models: Record<string, KiloModelEntry> = {};
|
|
79
|
+
for (const model of normalizeExportModels(ctx.models)) {
|
|
80
|
+
const entry: KiloModelEntry = { name: exportModelLabel(model) };
|
|
81
|
+
const context = authoritativeContextWindow(model.contextWindow);
|
|
82
|
+
if (context !== undefined) {
|
|
83
|
+
entry.limit = { context, output: outputBudgetFor(context, model) };
|
|
84
|
+
}
|
|
85
|
+
const capabilities = opencodeModelCapabilities(model.inputModalities);
|
|
86
|
+
if (capabilities) {
|
|
87
|
+
entry.attachment = capabilities.attachment;
|
|
88
|
+
entry.modalities = capabilities.modalities;
|
|
89
|
+
}
|
|
90
|
+
models[model.namespaced] = entry;
|
|
91
|
+
}
|
|
92
|
+
const headers = proxyAdmissionHeaders(config, KILO_API_KEY_ENV_REF);
|
|
93
|
+
return {
|
|
94
|
+
npm: "@ai-sdk/openai-compatible",
|
|
95
|
+
name: "OpenCodex",
|
|
96
|
+
options: headers ? { baseURL: ctx.baseUrl, headers } : { baseURL: ctx.baseUrl, apiKey: KILO_API_KEY_ENV_REF },
|
|
97
|
+
models,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildKiloClientConfig(ctx: ExportContext): KiloGeneratedConfig {
|
|
102
|
+
return {
|
|
103
|
+
$schema: KILO_CONFIG_SCHEMA,
|
|
104
|
+
provider: { [OPENCODE_PROVIDER_ID]: kiloProviderBlock(ctx) },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function summarizeKilo(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
109
|
+
const models = Object.values((document as KiloGeneratedConfig | undefined)?.provider?.[OPENCODE_PROVIDER_ID]?.models ?? {});
|
|
110
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => !model.limit).length };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function buildKiloContribution(ctx: ExportContext): ManagedContribution {
|
|
114
|
+
const doc = buildKiloClientConfig(ctx);
|
|
115
|
+
return singleFragment("kilo", ["provider", OPENCODE_PROVIDER_ID], doc.provider[OPENCODE_PROVIDER_ID]);
|
|
116
|
+
}
|
|
@@ -31,7 +31,7 @@ import type { OcxConfig } from "../types";
|
|
|
31
31
|
|
|
32
32
|
export type { ConfigFormat } from "../integrations/serialize";
|
|
33
33
|
export type { ManagedFragment, ManagedContribution, BuildContribution, OpencodeLaunchEnv, OpencodeCatalogModel, ExportModel, ExportContext, ExportClientId, ExportClientSpec, PiModelEntry } from "./config-export/contracts";
|
|
34
|
-
export { OPENCODE_PROVIDER_ID, OPENCODE_CONFIG_SCHEMA, OPENCODE_API_KEY_ENV, OPENCODE_API_KEY_ENV_REF, HERMES_API_KEY_ENV, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV, OPENCLAW_API_KEY_ENV_REF, LOOPBACK_API_KEY_PLACEHOLDER, GAJAE_API_KEY_ENV, SCHEMA_REQUIRED_OUTPUT_BUDGET, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG } from "./config-export/constants";
|
|
34
|
+
export { OPENCODE_PROVIDER_ID, OPENCODE_CONFIG_SCHEMA, OPENCODE_API_KEY_ENV, OPENCODE_API_KEY_ENV_REF, KILO_API_KEY_ENV, KILO_API_KEY_ENV_REF, KILO_CONFIG_SCHEMA, HERMES_API_KEY_ENV, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV, OPENCLAW_API_KEY_ENV_REF, LOOPBACK_API_KEY_PLACEHOLDER, GAJAE_API_KEY_ENV, SCHEMA_REQUIRED_OUTPUT_BUDGET, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG } from "./config-export/constants";
|
|
35
35
|
export { normalizeExportModels } from "./config-export/model-metadata";
|
|
36
36
|
export type { OmpModelEntry, OmpProviderBlock, OmpGeneratedConfig } from "./config-export/omp";
|
|
37
37
|
export type { ZcodeModelEntry, ZcodeProviderBlock, ZcodeGeneratedConfig } from "./config-export/zcode";
|
|
@@ -51,9 +51,11 @@ export type { DshReasoningEffort, DshWireReasoningEffort, DshModelEntry, DshProv
|
|
|
51
51
|
export type { McodeProviderBlock, McodeModelEntry, McodeGeneratedConfig } from "./config-export/mcode";
|
|
52
52
|
export type { RaycastAbility, RaycastAbilityName, RaycastModelEntry, RaycastProviderEntry, RaycastGeneratedConfig } from "./config-export/raycast";
|
|
53
53
|
export { buildRaycastClientConfig, summarizeRaycast, buildRaycastContribution } from "./config-export/raycast";
|
|
54
|
+
export { droidHomeDir, droidConfigPath, buildDroidClientConfig, summarizeDroid, buildDroidContribution } from "./config-export/droid";
|
|
55
|
+
export type { DroidModelEntry, DroidGeneratedConfig } from "./config-export/droid";
|
|
54
56
|
|
|
55
57
|
import type { OpencodeLaunchEnv, OpencodeCatalogModel, ExportContext, PiModelEntry, ManagedContribution, ManagedFragment, ExportClientId, ExportClientSpec } from "./config-export/contracts";
|
|
56
|
-
import { OPENCODE_API_KEY_ENV_REF, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG, OPENCODE_CONFIG_SCHEMA, OPENCODE_PROVIDER_ID, PI_API_DIALECT, LOOPBACK_API_KEY_PLACEHOLDER, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV_REF, OPENCODE_API_KEY_ENV, HERMES_API_KEY_ENV, OPENCLAW_API_KEY_ENV } from "./config-export/constants";
|
|
58
|
+
import { OPENCODE_API_KEY_ENV_REF, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG, OPENCODE_CONFIG_SCHEMA, OPENCODE_PROVIDER_ID, PI_API_DIALECT, LOOPBACK_API_KEY_PLACEHOLDER, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV_REF, OPENCODE_API_KEY_ENV, HERMES_API_KEY_ENV, OPENCLAW_API_KEY_ENV, KILO_API_KEY_ENV } from "./config-export/constants";
|
|
57
59
|
import { exportModelLabel, authoritativeContextWindow, outputBudgetFor, normalizeExportModels, inputModalitiesForClient, opencodeModelCapabilities, proxyAdmissionHeaders, singleFragment } from "./config-export/model-metadata";
|
|
58
60
|
import { buildOmpClientConfig, summarizeOmp, buildOmpContribution } from "./config-export/omp";
|
|
59
61
|
import { buildDshClientConfig, summarizeDsh, buildDshContribution } from "./config-export/dsh";
|
|
@@ -61,6 +63,10 @@ import { buildMcodeClientConfig, summarizeMcode, buildMcodeContribution } from "
|
|
|
61
63
|
import { buildZcodeClientConfig, summarizeZcode, buildZcodeContribution } from "./config-export/zcode";
|
|
62
64
|
import { buildClineClientConfig, summarizeCline, buildClineContribution } from "./config-export/cline";
|
|
63
65
|
import { buildRaycastClientConfig, summarizeRaycast, buildRaycastContribution } from "./config-export/raycast";
|
|
66
|
+
import { buildKiloClientConfig, summarizeKilo, buildKiloContribution, kiloConfigPath } from "./config-export/kilo";
|
|
67
|
+
export { kiloConfigPath, kiloHomeDir, kiloCandidatePath, KILO_CONFIG_CANDIDATES } from "./config-export/kilo";
|
|
68
|
+
export type { KiloGeneratedConfig, KiloProviderBlock, KiloModelEntry } from "./config-export/kilo";
|
|
69
|
+
import { droidConfigPath, buildDroidClientConfig, summarizeDroid, buildDroidContribution } from "./config-export/droid";
|
|
64
70
|
|
|
65
71
|
|
|
66
72
|
|
|
@@ -1588,6 +1594,31 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
|
1588
1594
|
buildContribution: buildClineContribution,
|
|
1589
1595
|
loopbackOnly: true,
|
|
1590
1596
|
},
|
|
1597
|
+
kilo: {
|
|
1598
|
+
id: "kilo",
|
|
1599
|
+
filename: "kilo.jsonc",
|
|
1600
|
+
destination: env => kiloConfigPath(env),
|
|
1601
|
+
apiKeyEnv: KILO_API_KEY_ENV,
|
|
1602
|
+
exportHint: `export ${KILO_API_KEY_ENV}=<your key>`,
|
|
1603
|
+
build: buildKiloClientConfig,
|
|
1604
|
+
format: "json",
|
|
1605
|
+
summarize: summarizeKilo,
|
|
1606
|
+
buildContribution: buildKiloContribution,
|
|
1607
|
+
loopbackOnly: false,
|
|
1608
|
+
jsonc: true,
|
|
1609
|
+
},
|
|
1610
|
+
droid: {
|
|
1611
|
+
id: "droid",
|
|
1612
|
+
filename: "factory-settings.json",
|
|
1613
|
+
destination: env => droidConfigPath(env),
|
|
1614
|
+
apiKeyEnv: "",
|
|
1615
|
+
exportHint: "Factory Droid reads keyless loopback custom models from settings.json. Select one with /model.",
|
|
1616
|
+
build: buildDroidClientConfig,
|
|
1617
|
+
format: "json",
|
|
1618
|
+
summarize: summarizeDroid,
|
|
1619
|
+
buildContribution: buildDroidContribution,
|
|
1620
|
+
loopbackOnly: true,
|
|
1621
|
+
},
|
|
1591
1622
|
};
|
|
1592
1623
|
|
|
1593
1624
|
export const EXPORT_CLIENT_IDS: readonly ExportClientId[] = Object.keys(EXPORT_CLIENTS) as ExportClientId[];
|
|
@@ -213,7 +213,7 @@ export async function handleCodexAuthAPI(
|
|
|
213
213
|
if (body.accountId === MAIN_CODEX_ACCOUNT_ID && hasLegacyMainCodexPoolAccount(runtimeConfig.codexAccounts)) {
|
|
214
214
|
return jsonResponse({ error: "Remove the legacy __main__ pool row before selecting the Desktop account" }, 409);
|
|
215
215
|
}
|
|
216
|
-
if (isCodexAccountPaused(runtimeConfig, targetAccountId)) {
|
|
216
|
+
if (body.accountId != null && isCodexAccountPaused(runtimeConfig, targetAccountId)) {
|
|
217
217
|
return jsonResponse({ error: "Account is paused" }, 409);
|
|
218
218
|
}
|
|
219
219
|
if (body.accountId != null && body.accountId !== MAIN_CODEX_ACCOUNT_ID) {
|
|
@@ -6,7 +6,7 @@ import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
|
|
|
6
6
|
import { isNativeAliasCatalogEntry, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./metadata";
|
|
7
7
|
import type { RawEntry } from "./parsing";
|
|
8
8
|
|
|
9
|
-
/** Project
|
|
9
|
+
/** Project access programs per native account, and availability prompts only onto bare main rows. */
|
|
10
10
|
export function applyNativeAccessPrograms(
|
|
11
11
|
entries: RawEntry[],
|
|
12
12
|
snapshot: CodexModelEntitlementSnapshot,
|
|
@@ -15,9 +15,13 @@ export function applyNativeAccessPrograms(
|
|
|
15
15
|
for (const entry of entries) {
|
|
16
16
|
// A combo may deliberately claim a bare native slug. It is still a routed combo row,
|
|
17
17
|
// so the matching native Codex roster must not project its access programs onto it.
|
|
18
|
-
if (isNativeAliasCatalogEntry(entry))
|
|
18
|
+
if (isNativeAliasCatalogEntry(entry)) {
|
|
19
|
+
delete entry.availability_nux;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
19
22
|
if (entry.owned_by === COMBO_NAMESPACE) {
|
|
20
23
|
delete entry.available_access_programs;
|
|
24
|
+
delete entry.availability_nux;
|
|
21
25
|
continue;
|
|
22
26
|
}
|
|
23
27
|
const accountBoundSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
@@ -25,10 +29,18 @@ export function applyNativeAccessPrograms(
|
|
|
25
29
|
&& SUPPORTED_NATIVE_OPENAI_SLUGS.has(entry.slug) ? entry.slug : undefined;
|
|
26
30
|
const slug = accountBoundSlug ?? bareSlug;
|
|
27
31
|
if (!slug) continue;
|
|
32
|
+
if (accountBoundSlug) delete entry.availability_nux;
|
|
28
33
|
const selector = accountBoundSlug && typeof entry.slug === "string"
|
|
29
34
|
? entry.slug.slice(0, entry.slug.indexOf("/")) : undefined;
|
|
30
35
|
const target = selector === undefined ? MAIN_CODEX_ACCOUNT_ID : accountTargets.get(selector);
|
|
31
36
|
const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
|
|
37
|
+
if (bareSlug) {
|
|
38
|
+
entry.availability_nux = accountId === MAIN_CODEX_ACCOUNT_ID
|
|
39
|
+
&& snapshot.confirmedAccountIds.has(accountId)
|
|
40
|
+
&& snapshot.modelsByAccount.get(accountId)?.has(slug)
|
|
41
|
+
? snapshot.availabilityNuxByAccount?.get(accountId)?.get(slug) ?? null
|
|
42
|
+
: null;
|
|
43
|
+
}
|
|
32
44
|
if (!accountId) {
|
|
33
45
|
delete entry.available_access_programs;
|
|
34
46
|
continue;
|
|
@@ -231,7 +231,7 @@ export async function fetchProviderModelsWithAuth(
|
|
|
231
231
|
// KIRO_OBSERVED_CATALOG_MAX of them. Every observed id still informs routing preference.
|
|
232
232
|
let observedAdded = 0;
|
|
233
233
|
for (const account of getAccountSet("kiro")?.accounts ?? []) {
|
|
234
|
-
if (account.needsReauth === true) continue;
|
|
234
|
+
if (account.needsReauth === true || account.paused === true) continue;
|
|
235
235
|
for (const row of readKiroAccountModels(account) ?? []) {
|
|
236
236
|
if (observedAdded >= KIRO_OBSERVED_CATALOG_MAX) break;
|
|
237
237
|
if (!KIRO_PUBLISHABLE_MODEL_ID.test(row.modelId) || ids.includes(row.modelId)) continue;
|