@bitkyc08/opencodex 2.8.0 → 2.8.2-preview.20260731
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/{index-BDjpkcRN.js → index-GC0Vlu1Z.js} +2 -2
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +42 -7
- package/src/adapters/openai-chat.ts +55 -4
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/init.ts +129 -102
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog.ts +2 -2
- package/src/lib/destination-policy.ts +12 -1
- package/src/lib/provider-outbound.ts +3 -0
- package/src/lib/winsw.ts +6 -0
- package/src/oauth/kiro-credentials.ts +72 -1
- package/src/oauth/kiro.ts +13 -2
- package/src/providers/free-directory.ts +4 -1
- package/src/server/index.ts +3 -3
- package/src/server/management/agent-settings-routes.ts +4 -4
- package/src/server/management/config-routes.ts +5 -0
- package/src/server/management/model-routes.ts +15 -1
- package/src/server/management/shared.ts +16 -3
- package/src/server/proxy-liveness.ts +9 -2
- package/src/service.ts +159 -13
- package/src/tray/windows.ts +54 -9
- package/src/types.ts +5 -0
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-GC0Vlu1Z.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-BHsKRFh9.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitkyc08/opencodex",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.2-preview.20260731",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
@@ -250,6 +250,40 @@ function usesNativeAnthropicEndpoint(provider: OcxProviderConfig): boolean {
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/** Normalize provider baseUrl paths ending in `/`, `/v1`, or `/v1/messages` to `{origin}/v1/messages`. */
|
|
254
|
+
export function anthropicMessagesUrl(baseUrl: string): string {
|
|
255
|
+
try {
|
|
256
|
+
new URL(baseUrl);
|
|
257
|
+
} catch {
|
|
258
|
+
throw new Error(`anthropic provider has malformed baseUrl: ${baseUrl}`);
|
|
259
|
+
}
|
|
260
|
+
const trimmed = baseUrl.trim().replace(/\/+$/, "");
|
|
261
|
+
const root = trimmed.replace(/\/v1\/messages\/?$/i, "").replace(/\/v1\/?$/i, "").replace(/\/+$/, "");
|
|
262
|
+
return `${root}/v1/messages`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function synthesizeToolUseId(): string {
|
|
266
|
+
return `toolu_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function toolUseArguments(input: unknown): string {
|
|
270
|
+
if (typeof input === "string") {
|
|
271
|
+
const trimmed = input.trim();
|
|
272
|
+
if (!trimmed) return "{}";
|
|
273
|
+
try {
|
|
274
|
+
JSON.parse(trimmed);
|
|
275
|
+
return trimmed;
|
|
276
|
+
} catch {
|
|
277
|
+
// A tool call's arguments must be a JSON object. Re-encoding an unparseable string as a
|
|
278
|
+
// JSON *string* is the double-encoding #765 reports: the caller then receives
|
|
279
|
+
// `"get weather"` where an object was required and the tool call is unusable either way.
|
|
280
|
+
// An empty object at least fails in the tool's own argument validation.
|
|
281
|
+
return "{}";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return JSON.stringify(input ?? {});
|
|
285
|
+
}
|
|
286
|
+
|
|
253
287
|
function anthropicKeyUsesBearer(provider: OcxProviderConfig): boolean {
|
|
254
288
|
return provider.apiKeyTransport === "bearer";
|
|
255
289
|
}
|
|
@@ -680,8 +714,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
680
714
|
else if (typeof tc === "object" && "name" in tc) body.tool_choice = { type: "tool", name: toolNames.toWire(resolveToolChoiceWireName(parsed.context.tools, tc.name)) };
|
|
681
715
|
}
|
|
682
716
|
|
|
683
|
-
const
|
|
684
|
-
const url = `${base}/v1/messages`;
|
|
717
|
+
const url = anthropicMessagesUrl(provider.baseUrl);
|
|
685
718
|
const unresolvedPlaceholder = url.match(/\{[^}]*\}/)?.[0];
|
|
686
719
|
if (unresolvedPlaceholder) {
|
|
687
720
|
throw new Error(`anthropic baseUrl contains unresolved ${unresolvedPlaceholder}`);
|
|
@@ -773,7 +806,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
773
806
|
if (!block) break;
|
|
774
807
|
currentBlockType = block.type;
|
|
775
808
|
if (block.type === "tool_use") {
|
|
776
|
-
currentToolCallId = block.id ??
|
|
809
|
+
currentToolCallId = block.id ?? synthesizeToolUseId();
|
|
777
810
|
currentToolCallName = toolNames.fromWire(block.name ?? "");
|
|
778
811
|
yield { type: "tool_call_start", id: currentToolCallId, name: currentToolCallName };
|
|
779
812
|
}
|
|
@@ -799,7 +832,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
799
832
|
// Arrives once, just before the thinking block's content_block_stop; block-scoped
|
|
800
833
|
// so a stray signature on a non-thinking block can never be captured.
|
|
801
834
|
yield { type: "thinking_signature", signature: delta.signature };
|
|
802
|
-
} else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
|
|
835
|
+
} else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string" && currentBlockType === "tool_use") {
|
|
803
836
|
yield { type: "tool_call_delta", arguments: delta.partial_json };
|
|
804
837
|
}
|
|
805
838
|
break;
|
|
@@ -831,12 +864,13 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
831
864
|
}
|
|
832
865
|
}
|
|
833
866
|
if (!emittedDone) {
|
|
867
|
+
// Fail closed on transport EOF. Compatible providers may omit message_stop after message_delta.stop_reason.
|
|
834
868
|
if (pendingStopReason !== undefined) {
|
|
835
869
|
const stopReason = pendingStopReason === "max_tokens"
|
|
836
870
|
? "max_tokens"
|
|
837
871
|
: pendingStopReason === "refusal" || pendingStopReason === "content_filter"
|
|
838
872
|
? "content_filter"
|
|
839
|
-
:
|
|
873
|
+
: pendingStopReason;
|
|
840
874
|
emittedDone = true;
|
|
841
875
|
yield {
|
|
842
876
|
type: "done",
|
|
@@ -867,8 +901,9 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
867
901
|
} else if (block.type === "redacted_thinking" && typeof block.data === "string") {
|
|
868
902
|
events.push({ type: "redacted_thinking", data: block.data });
|
|
869
903
|
} else if (block.type === "tool_use") {
|
|
870
|
-
|
|
871
|
-
events.push({ type: "
|
|
904
|
+
const id = block.id ?? synthesizeToolUseId();
|
|
905
|
+
events.push({ type: "tool_call_start", id, name: toolNames.fromWire(block.name ?? "") });
|
|
906
|
+
events.push({ type: "tool_call_delta", arguments: toolUseArguments(block.input) });
|
|
872
907
|
events.push({ type: "tool_call_end" });
|
|
873
908
|
}
|
|
874
909
|
}
|
|
@@ -206,10 +206,10 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
206
206
|
}));
|
|
207
207
|
// "" instead of null: strict validators (xAI: "Each message must have at least one
|
|
208
208
|
// content element", langchain#34140) reject content-less assistant history entries.
|
|
209
|
-
if (!chatMsg.content) chatMsg.content =
|
|
209
|
+
if (!chatMsg.content) chatMsg.content = emptyAssistantContent(provider);
|
|
210
210
|
}
|
|
211
211
|
if (chatMsg.reasoning_content !== undefined && chatMsg.content === undefined && chatMsg.tool_calls === undefined) {
|
|
212
|
-
chatMsg.content =
|
|
212
|
+
chatMsg.content = emptyAssistantContent(provider);
|
|
213
213
|
}
|
|
214
214
|
out.push(chatMsg);
|
|
215
215
|
pendingToolCalls = wireToolCalls.map(({ tc, id }) => ({ id, name: namespacedToolName(tc.namespace, tc.name) }));
|
|
@@ -238,7 +238,7 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
238
238
|
const name = safeToolName(msg.toolName);
|
|
239
239
|
out.push({
|
|
240
240
|
role: "assistant",
|
|
241
|
-
content:
|
|
241
|
+
content: emptyAssistantContent(provider),
|
|
242
242
|
tool_calls: [{
|
|
243
243
|
id: toolCallId,
|
|
244
244
|
type: "function",
|
|
@@ -372,6 +372,43 @@ function isKimiSchemaTarget(provider: OcxProviderConfig): boolean {
|
|
|
372
372
|
}
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
// Volcengine Ark regional endpoints. Ark validates an assistant message's text field as a
|
|
376
|
+
// REQUIRED parameter and treats "" as absent, so a tool-call-only assistant in history 400s with
|
|
377
|
+
// `MissingParameter: input.content.text` (#796). Every other OpenAI-compatible provider accepts
|
|
378
|
+
// "", and xAI actively requires it ("Each message must have at least one content element"), so
|
|
379
|
+
// the two contracts are in direct conflict and this cannot be a global change.
|
|
380
|
+
const VOLCENGINE_ARK_HOSTNAMES = new Set([
|
|
381
|
+
"ark.cn-beijing.volces.com",
|
|
382
|
+
"ark.ap-southeast.volces.com",
|
|
383
|
+
]);
|
|
384
|
+
|
|
385
|
+
function isVolcengineArkTarget(provider: OcxProviderConfig): boolean {
|
|
386
|
+
try {
|
|
387
|
+
return VOLCENGINE_ARK_HOSTNAMES.has(new URL(provider.baseUrl).hostname);
|
|
388
|
+
} catch {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Placeholder content for an assistant history entry carrying only tool calls or reasoning.
|
|
395
|
+
*
|
|
396
|
+
* UNVERIFIED HYPOTHESIS for Ark. The reported error names `input.content.text`, a nested path,
|
|
397
|
+
* which suggests Ark wants the structured content form `[{type:"text",text:""}]` rather than a
|
|
398
|
+
* bare string — no string value, `""` or `" "`, exposes a `content.text` path at all. But Ark's
|
|
399
|
+
* published examples only show array content for MULTIMODAL USER input, never for an assistant
|
|
400
|
+
* history entry, so this shape is inferred from the error message and not confirmed by the docs
|
|
401
|
+
* or by a live request. The empty inner text at least adds no tokens either way.
|
|
402
|
+
*
|
|
403
|
+
* Confirm against a real Ark endpoint before relying on this; #796 records what is still missing.
|
|
404
|
+
*
|
|
405
|
+
* Every other provider keeps the bare `""`, which xAI's validator specifically requires ("Each
|
|
406
|
+
* message must have at least one content element"), so this cannot be applied globally.
|
|
407
|
+
*/
|
|
408
|
+
function emptyAssistantContent(provider: OcxProviderConfig): string | { type: "text"; text: string }[] {
|
|
409
|
+
return isVolcengineArkTarget(provider) ? [{ type: "text", text: "" }] : "";
|
|
410
|
+
}
|
|
411
|
+
|
|
375
412
|
/**
|
|
376
413
|
* Kimi requires function.parameters.type to be exactly "object" at the root.
|
|
377
414
|
* Codex tools with oneOf/anyOf schemas omit the root type, causing 400 errors.
|
|
@@ -800,12 +837,26 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
800
837
|
if (buffer.length > 0) {
|
|
801
838
|
if ((yield* handleDataLine(buffer)) === "terminate") return;
|
|
802
839
|
}
|
|
803
|
-
yield* flushToolCalls();
|
|
804
840
|
// Reader EOF. A graceful close shows at least one terminal signal: `[DONE]` (returns above),
|
|
805
841
|
// a non-null finish_reason (sawFinish), or a trailing usage chunk (providers emit usage only
|
|
806
842
|
// at end-of-generation). If NONE of those were seen, the stream was cut mid-flight — fail
|
|
807
843
|
// closed so the bridge emits a classified response.failed rather than a silent truncation.
|
|
844
|
+
//
|
|
845
|
+
// Checked BEFORE flushToolCalls(), because that helper emits tool_call_end and there is no
|
|
846
|
+
// taking it back: a half-assembled argument string would reach the client as a completed
|
|
847
|
+
// call. Tool calls are buffered here (unlike the Anthropic adapter, which forwards
|
|
848
|
+
// fragments live), so this adapter can still decide.
|
|
808
849
|
const sawFinish = finishReason !== undefined;
|
|
850
|
+
if (!sawFinish && pendingToolCalls.length > 0) {
|
|
851
|
+
debugProviderDiagnostic("openai-chat", "stream-truncated", {
|
|
852
|
+
finishReason: null,
|
|
853
|
+
hadUsage: pendingUsage !== undefined,
|
|
854
|
+
pendingToolCalls: pendingToolCalls.length,
|
|
855
|
+
});
|
|
856
|
+
yield { type: "error", message: "upstream stream ended mid tool call without a terminal signal — possible truncation" };
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
yield* flushToolCalls();
|
|
809
860
|
if (!sawFinish && pendingUsage === undefined) {
|
|
810
861
|
debugProviderDiagnostic("openai-chat", "stream-truncated", {
|
|
811
862
|
finishReason: finishReason ?? null,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type DesktopProfile,
|
|
11
11
|
} from "../claude/desktop-profile";
|
|
12
12
|
import { writeDesktop3pConfig, type Desktop3pConfigMode, parseDesktop3pModeArgs } from "../claude/desktop-3p";
|
|
13
|
-
import { filterCatalogVisibleModels,
|
|
13
|
+
import { filterCatalogVisibleModels, desktopVisibleNativeSlugs } from "../codex/catalog";
|
|
14
14
|
import { buildClaudeDesktopState, fetchAllModels } from "../server/management-api";
|
|
15
15
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
16
16
|
|
|
@@ -42,7 +42,7 @@ async function applyProfile(profile: DesktopProfile, mode: Desktop3pConfigMode):
|
|
|
42
42
|
}));
|
|
43
43
|
const result = writeDesktop3pConfig(
|
|
44
44
|
live?.port ?? config.port ?? 10100,
|
|
45
|
-
[...
|
|
45
|
+
[...desktopVisibleNativeSlugs(config)],
|
|
46
46
|
routed,
|
|
47
47
|
config.apiKeys?.[0]?.key,
|
|
48
48
|
mode,
|
package/src/cli/init.ts
CHANGED
|
@@ -8,11 +8,28 @@ import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
|
8
8
|
|
|
9
9
|
function createPrompt(): { ask(question: string): Promise<string>; close(): void } {
|
|
10
10
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
11
|
+
let closed = false;
|
|
12
|
+
rl.on("close", () => { closed = true; });
|
|
11
13
|
return {
|
|
12
14
|
ask(question: string): Promise<string> {
|
|
13
|
-
return new Promise(resolve =>
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
if (closed) {
|
|
17
|
+
reject(new Error("stdin closed before the prompt could be answered"));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const onClose = () => {
|
|
21
|
+
reject(new Error("stdin reached EOF while waiting for input"));
|
|
22
|
+
};
|
|
23
|
+
rl.once("close", onClose);
|
|
24
|
+
rl.question(question, answer => {
|
|
25
|
+
rl.off("close", onClose);
|
|
26
|
+
resolve(answer);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
close() {
|
|
31
|
+
if (!closed) rl.close();
|
|
14
32
|
},
|
|
15
|
-
close() { rl.close(); },
|
|
16
33
|
};
|
|
17
34
|
}
|
|
18
35
|
|
|
@@ -83,115 +100,125 @@ export function cleanupOpenAiTierBackupAfterInit(configPath = getConfigPath()):
|
|
|
83
100
|
|
|
84
101
|
export async function runInit(): Promise<void> {
|
|
85
102
|
const prompt = createPrompt();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
103
|
+
try {
|
|
104
|
+
console.log("\n🔧 opencodex (ocx) setup\n");
|
|
105
|
+
|
|
106
|
+
const providers = buildInitProviders();
|
|
107
|
+
printMenu(providers);
|
|
108
|
+
|
|
109
|
+
const choice = await prompt.ask("\nSelect default provider (number): ");
|
|
110
|
+
const idx = parseInt(choice, 10) - 1;
|
|
111
|
+
|
|
112
|
+
let providerName: string;
|
|
113
|
+
let providerConfig: OcxProviderConfig;
|
|
114
|
+
let oauthHint = false;
|
|
115
|
+
|
|
116
|
+
if (idx >= 0 && idx < providers.length) {
|
|
117
|
+
const p = providers[idx];
|
|
118
|
+
providerName = p.id;
|
|
119
|
+
console.log(`\n📡 ${p.label}`);
|
|
120
|
+
console.log(` Base URL: ${p.baseUrl}`);
|
|
121
|
+
|
|
122
|
+
if (p.kind === "forward") {
|
|
123
|
+
providerConfig = { adapter: p.adapter, baseUrl: p.baseUrl, authMode: "forward" };
|
|
124
|
+
console.log(" No API key needed — forwards your existing `codex login`.");
|
|
125
|
+
} else if (p.kind === "oauth") {
|
|
126
|
+
providerConfig = { adapter: p.adapter, baseUrl: p.baseUrl, authMode: "oauth", ...(p.defaultModel ? { defaultModel: p.defaultModel } : {}) };
|
|
127
|
+
oauthHint = true;
|
|
128
|
+
} else {
|
|
129
|
+
// key + local: collect a key (local usually blank).
|
|
130
|
+
if (p.dashboardUrl) console.log(` 🔑 Get your key: ${p.dashboardUrl}`);
|
|
131
|
+
// Template URL with placeholders (e.g. Cloudflare's {account_id}) needs a resolved value.
|
|
132
|
+
let baseUrl = p.baseUrl;
|
|
133
|
+
if (/\{[^}]*\}/.test(baseUrl)) {
|
|
134
|
+
const resolved = (await prompt.ask(` Your endpoint URL (${baseUrl}): `)).trim();
|
|
135
|
+
if (!resolved) {
|
|
136
|
+
console.error(" A resolved URL is required — replace the {placeholder} with your actual value.");
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
baseUrl = resolved;
|
|
120
140
|
}
|
|
121
|
-
|
|
141
|
+
const env = envKeyFor(p.id);
|
|
142
|
+
const hint = p.kind === "local" ? "API key (usually blank — press Enter): " : `API key (paste, or env var $${env}): `;
|
|
143
|
+
const apiKey = (await prompt.ask(`\n${hint}`)).trim();
|
|
144
|
+
const modelChoice = (await prompt.ask(`Default model${p.defaultModel ? ` [${p.defaultModel}]` : " (optional)"}: `)).trim();
|
|
145
|
+
const defaultModel = modelChoice || p.defaultModel;
|
|
146
|
+
providerConfig = {
|
|
147
|
+
adapter: p.adapter,
|
|
148
|
+
baseUrl,
|
|
149
|
+
...(p.kind === "key" ? { apiKey: apiKey || `\${${env}}` } : apiKey ? { apiKey } : {}),
|
|
150
|
+
...(defaultModel ? { defaultModel } : {}),
|
|
151
|
+
};
|
|
152
|
+
// Apply the catalog's models / vision classification (same enrichment as the GUI).
|
|
153
|
+
enrichProviderFromCatalog(p.id, providerConfig);
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
providerName = (await prompt.ask("Provider name: ")).trim();
|
|
157
|
+
if (!isValidProviderName(providerName)) {
|
|
158
|
+
console.error("Provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key.");
|
|
159
|
+
process.exit(1);
|
|
122
160
|
}
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const apiKey =
|
|
126
|
-
const
|
|
127
|
-
const defaultModel = modelChoice || p.defaultModel;
|
|
161
|
+
const baseUrl = await prompt.ask("Base URL (e.g. http://localhost:11434/v1): ");
|
|
162
|
+
const adapter = await prompt.ask("Adapter [openai-chat]: ") || "openai-chat";
|
|
163
|
+
const apiKey = await prompt.ask("API key (optional): ");
|
|
164
|
+
const defaultModel = await prompt.ask("Default model: ");
|
|
128
165
|
providerConfig = {
|
|
129
|
-
adapter:
|
|
130
|
-
baseUrl,
|
|
131
|
-
...(
|
|
132
|
-
...(defaultModel ? { defaultModel } : {}),
|
|
166
|
+
adapter: adapter.trim(),
|
|
167
|
+
baseUrl: baseUrl.trim(),
|
|
168
|
+
...(apiKey.trim() ? { apiKey: apiKey.trim() } : {}),
|
|
169
|
+
...(defaultModel.trim() ? { defaultModel: defaultModel.trim() } : {}),
|
|
133
170
|
};
|
|
134
|
-
// Apply the catalog's models / vision classification (same enrichment as the GUI).
|
|
135
|
-
enrichProviderFromCatalog(p.id, providerConfig);
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
providerName = (await prompt.ask("Provider name: ")).trim();
|
|
139
|
-
if (!isValidProviderName(providerName)) {
|
|
140
|
-
console.error("Provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key.");
|
|
141
|
-
prompt.close();
|
|
142
|
-
process.exit(1);
|
|
143
171
|
}
|
|
144
|
-
const baseUrl = await prompt.ask("Base URL (e.g. http://localhost:11434/v1): ");
|
|
145
|
-
const adapter = await prompt.ask("Adapter [openai-chat]: ") || "openai-chat";
|
|
146
|
-
const apiKey = await prompt.ask("API key (optional): ");
|
|
147
|
-
const defaultModel = await prompt.ask("Default model: ");
|
|
148
|
-
providerConfig = {
|
|
149
|
-
adapter: adapter.trim(),
|
|
150
|
-
baseUrl: baseUrl.trim(),
|
|
151
|
-
...(apiKey.trim() ? { apiKey: apiKey.trim() } : {}),
|
|
152
|
-
...(defaultModel.trim() ? { defaultModel: defaultModel.trim() } : {}),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
172
|
|
|
156
|
-
|
|
157
|
-
|
|
173
|
+
const portStr = await prompt.ask("\nProxy port [10100]: ");
|
|
174
|
+
const port = parseInt(portStr, 10) || 10100;
|
|
158
175
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
176
|
+
const config: OcxConfig = {
|
|
177
|
+
...getDefaultConfig(),
|
|
178
|
+
port,
|
|
179
|
+
providers: { [providerName]: providerConfig },
|
|
180
|
+
defaultProvider: providerName,
|
|
181
|
+
};
|
|
165
182
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
+
saveConfig(config);
|
|
184
|
+
// Init writes a fresh config, so a stale pre-migration backup from a previous
|
|
185
|
+
// installation would make the next `ocx start` crash on a stale-backup
|
|
186
|
+
// collision (issue #257). But only a STALE backup (unparseable, or already a
|
|
187
|
+
// post-migration v2 snapshot) may be deleted; a backup that still parses as a
|
|
188
|
+
// valid pre-migration (v1) config is a user-intentional rollback point and is
|
|
189
|
+
// preserved by renaming it out of the collision path (sol review 260722).
|
|
190
|
+
cleanupOpenAiTierBackupAfterInit();
|
|
191
|
+
console.log(`\n✅ Config saved to ~/.opencodex/config.json`);
|
|
192
|
+
if (oauthHint) console.log(`🔐 Authenticate this provider with: ocx login ${providerName}`);
|
|
193
|
+
|
|
194
|
+
const injectAnswer = await prompt.ask("Inject into Codex config.toml? [Y/n]: ");
|
|
195
|
+
if (injectAnswer.trim().toLowerCase() !== "n") {
|
|
196
|
+
console.log("Fetching available models from provider...");
|
|
197
|
+
const result = await injectCodexConfig(port, config);
|
|
198
|
+
console.log(result.success ? `✅ ${result.message}` : `⚠️ ${result.message}`);
|
|
199
|
+
}
|
|
183
200
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
201
|
+
const shimAnswer = await prompt.ask("Install Codex autostart shim? [Y/n]: ");
|
|
202
|
+
if (shimAnswer.trim().toLowerCase() !== "n") {
|
|
203
|
+
try {
|
|
204
|
+
const { installCodexShim } = await import("../codex/shim");
|
|
205
|
+
const result = installCodexShim();
|
|
206
|
+
console.log(result.installed ? `✅ ${result.message}` : `⚠️ ${result.message}`);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.log(`⚠️ Codex autostart shim skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
209
|
+
}
|
|
192
210
|
}
|
|
193
|
-
}
|
|
194
211
|
|
|
195
|
-
|
|
196
|
-
|
|
212
|
+
console.log(`\n🚀 Setup complete! Run 'ocx start' to start the proxy.`);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
215
|
+
if (/stdin (closed|reached EOF)/i.test(message)) {
|
|
216
|
+
console.error(`\n❌ ${message}. Re-run \`ocx init\` in an interactive terminal.`);
|
|
217
|
+
process.exitCode = 1;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
throw error;
|
|
221
|
+
} finally {
|
|
222
|
+
prompt.close();
|
|
223
|
+
}
|
|
197
224
|
}
|
|
@@ -123,6 +123,12 @@ export function visibleNativeSlugs(config: Pick<OcxConfig, "disabledModels">): s
|
|
|
123
123
|
return nativeOpenAiSlugs().filter(slug => !disabled.has(slug));
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/** Native slugs exposed to Claude Desktop show/export/apply (opt-out via claudeCode.desktopNativeModels). */
|
|
127
|
+
export function desktopVisibleNativeSlugs(config: Pick<OcxConfig, "claudeCode" | "disabledModels">): string[] {
|
|
128
|
+
if (config.claudeCode?.desktopNativeModels === false) return [];
|
|
129
|
+
return visibleNativeSlugs(config);
|
|
130
|
+
}
|
|
131
|
+
|
|
126
132
|
export function nativeModelRows(config: Pick<OcxConfig, "disabledModels">): Array<{ slug: string; disabled: boolean; contextWindow?: number }> {
|
|
127
133
|
const disabled = disabledNativeSlugs(config);
|
|
128
134
|
return NATIVE_OPENAI_MODELS.map(slug => {
|
package/src/codex/catalog.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// AUTO-SPLIT facade: original catalog.ts body moved into ./catalog/* modules.
|
|
2
2
|
// Public surface preserved exactly; importers keep using "src/codex/catalog".
|
|
3
|
-
export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing";
|
|
3
|
+
export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, readCatalog, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing";
|
|
4
4
|
export type { CatalogModel, MultiAgentMode } from "./catalog/parsing";
|
|
5
|
-
export { NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs } from "./catalog/metadata";
|
|
5
|
+
export { NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs } from "./catalog/metadata";
|
|
6
6
|
export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled";
|
|
7
7
|
export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, codexSupportedReasoningEfforts, clampedDefaultEffort, clampEntryToCodexSupportedEfforts, clampCatalogModelsToCodexSupport } from "./catalog/effort";
|
|
8
8
|
export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithJawcodeMetadata } from "./catalog/provider-fetch";
|
|
@@ -130,7 +130,18 @@ function registryAllowsPrivateNetwork(name: string): boolean {
|
|
|
130
130
|
return getProviderRegistryEntry(name)?.allowPrivateNetworkByDefault === true;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Whether a provider may reach loopback/private addresses.
|
|
135
|
+
*
|
|
136
|
+
* Two sources, and both have to be consulted at every boundary: the operator's explicit
|
|
137
|
+
* `allowPrivateNetwork`, and the registry's `allowPrivateNetworkByDefault` for entries that are
|
|
138
|
+
* local BY DEFINITION (Ollama, vLLM, LM Studio, LiteLLM). Config validation already read both;
|
|
139
|
+
* outbound discovery read only the first, so a stock Ollama entry passed validation and was then
|
|
140
|
+
* refused at the fetch (#758).
|
|
141
|
+
*
|
|
142
|
+
* This grants nothing new. Metadata, link-local and unspecified destinations are rejected before
|
|
143
|
+
* this is consulted, and a provider without either source still cannot reach a private address.
|
|
144
|
+
*/
|
|
134
145
|
export function providerAllowsPrivateNetwork(
|
|
135
146
|
name: string,
|
|
136
147
|
provider: Pick<OcxProviderConfig, "allowPrivateNetwork">,
|
|
@@ -117,6 +117,9 @@ export async function providerOutboundGet(
|
|
|
117
117
|
if (assessment?.kind === "metadata" || assessment?.kind === "link-local" || assessment?.kind === "unspecified") {
|
|
118
118
|
throw new ProviderOutboundPolicyError(`provider URL targets ${assessment.detail}`);
|
|
119
119
|
}
|
|
120
|
+
// Registry defaults count here too, not just the operator flag: a stock Ollama entry is
|
|
121
|
+
// local by definition and previously passed config validation only to be refused at the
|
|
122
|
+
// fetch (#758). Metadata/link-local/unspecified were already rejected above.
|
|
120
123
|
const allowPrivate = providerAllowsPrivateNetwork(name, provider);
|
|
121
124
|
if (!allowPrivate) {
|
|
122
125
|
const destinationError = providerDestinationConfigError(name, {
|
package/src/lib/winsw.ts
CHANGED
|
@@ -165,6 +165,12 @@ function runWinsw(args: string[]): string {
|
|
|
165
165
|
|
|
166
166
|
/** `install /p` prompts for the service-account password on the console — stdin must be inherited. */
|
|
167
167
|
function runWinswInteractive(args: string[]): void {
|
|
168
|
+
if (!process.stdin.isTTY) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
"WinSW install requires an interactive console to prompt for the service account password. "
|
|
171
|
+
+ "Run `ocx service install --native` from an elevated Command Prompt or PowerShell window, not a hidden or piped session.",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
168
174
|
execFileSync(winswExePath(), args, { stdio: "inherit" });
|
|
169
175
|
}
|
|
170
176
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { chmodSync, closeSync, existsSync, fsyncSync, linkSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, linkSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { isAbsolute, join, posix, win32 } from "node:path";
|
|
5
5
|
import { Database } from "bun:sqlite";
|
|
@@ -169,6 +169,77 @@ export function resolveKiroCliNativeSessionEntries(
|
|
|
169
169
|
return [{ location: "kiro-cli-linux-data", path: posix.join(home, ".local", "share", "kiro-cli", "data.sqlite3") }];
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Resolve the absolute kiro-cli executable for spawn/login helpers.
|
|
174
|
+
*
|
|
175
|
+
* Pure + parameterized like `resolveKiroCliNativeSessionEntries` so Windows install layouts can be
|
|
176
|
+
* covered from any host. PATH remains the first choice; only when bare `kiro-cli` is missing do we
|
|
177
|
+
* fall back to the platform-native install directories next to the session database.
|
|
178
|
+
*
|
|
179
|
+
* Windows: official MSI installs to `C:\Program Files\Kiro-Cli\kiro-cli.exe`, while some local
|
|
180
|
+
* installs keep the binary next to `%LOCALAPPDATA%\Kiro-Cli\data.sqlite3`.
|
|
181
|
+
* macOS/Linux: prefer PATH, then the usual user-local bin directories.
|
|
182
|
+
*/
|
|
183
|
+
export function resolveKiroCliExecutable(
|
|
184
|
+
inputs: KiroCliNativeInputs & {
|
|
185
|
+
pathEntries?: string[];
|
|
186
|
+
exists?: (path: string) => boolean;
|
|
187
|
+
isFile?: (path: string) => boolean;
|
|
188
|
+
},
|
|
189
|
+
): string {
|
|
190
|
+
const exists = inputs.exists ?? existsSync;
|
|
191
|
+
// A directory named `kiro-cli` on PATH satisfies existsSync and would then be handed to
|
|
192
|
+
// spawn(), which fails with EACCES at login instead of falling through to the next candidate.
|
|
193
|
+
// When a caller injects `exists` it owns the whole filesystem view, so the real stat would
|
|
194
|
+
// reject every synthetic path; such callers inject `isFile` too when they care about it.
|
|
195
|
+
const isFile = inputs.isFile ?? (inputs.exists ? () => true : ((path: string) => {
|
|
196
|
+
try {
|
|
197
|
+
return statSync(path).isFile();
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}));
|
|
202
|
+
const pathEntries = inputs.pathEntries
|
|
203
|
+
?? (inputs.env.PATH ?? inputs.env.Path ?? "").split(inputs.platform === "win32" ? ";" : ":")
|
|
204
|
+
.map(entry => entry.trim())
|
|
205
|
+
.filter(Boolean);
|
|
206
|
+
|
|
207
|
+
const pathCandidates = inputs.platform === "win32"
|
|
208
|
+
? pathEntries.flatMap(entry => [
|
|
209
|
+
win32.join(entry, "kiro-cli.exe"),
|
|
210
|
+
win32.join(entry, "kiro-cli"),
|
|
211
|
+
])
|
|
212
|
+
: pathEntries.map(entry => posix.join(entry, "kiro-cli"));
|
|
213
|
+
|
|
214
|
+
const installCandidates: string[] = [];
|
|
215
|
+
if (inputs.platform === "win32") {
|
|
216
|
+
const localBase = inputs.env.LOCALAPPDATA?.trim()
|
|
217
|
+
|| (inputs.env.USERPROFILE?.trim() ? win32.join(inputs.env.USERPROFILE.trim(), "AppData", "Local") : "")
|
|
218
|
+
|| win32.join(inputs.home, "AppData", "Local");
|
|
219
|
+
const programFiles = inputs.env["ProgramFiles"]?.trim() || "C:\\Program Files";
|
|
220
|
+
installCandidates.push(
|
|
221
|
+
win32.join(localBase, "Kiro-Cli", "kiro-cli.exe"),
|
|
222
|
+
win32.join(programFiles, "Kiro-Cli", "kiro-cli.exe"),
|
|
223
|
+
);
|
|
224
|
+
} else if (inputs.platform === "darwin") {
|
|
225
|
+
installCandidates.push(
|
|
226
|
+
posix.join(inputs.home, ".local", "bin", "kiro-cli"),
|
|
227
|
+
"/usr/local/bin/kiro-cli",
|
|
228
|
+
"/opt/homebrew/bin/kiro-cli",
|
|
229
|
+
);
|
|
230
|
+
} else {
|
|
231
|
+
installCandidates.push(
|
|
232
|
+
posix.join(inputs.home, ".local", "bin", "kiro-cli"),
|
|
233
|
+
"/usr/local/bin/kiro-cli",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const candidate of [...pathCandidates, ...installCandidates]) {
|
|
238
|
+
if (exists(candidate) && isFile(candidate)) return candidate;
|
|
239
|
+
}
|
|
240
|
+
return inputs.platform === "win32" ? "kiro-cli.exe" : "kiro-cli";
|
|
241
|
+
}
|
|
242
|
+
|
|
172
243
|
function nativeKiroCliSessionEntries(): Array<{ location: KiroCliNativeLocation; path: string }> {
|
|
173
244
|
// Only the stores that `kiro-cli logout` / `kiro-cli login` themselves mutate. Import fallbacks
|
|
174
245
|
// (Amazon Q / SSO cache) and KIROCLI_DB_PATH selectors must not be snapshotted for rollback.
|