@bitkyc08/opencodex 2.15.1 → 2.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{index-CMCDkQ7U.js → index-DOKr6RBR.js} +10 -10
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/base.ts +2 -0
- package/src/adapters/google-antigravity-replay.ts +9 -1
- package/src/adapters/kiro-thinking.ts +8 -0
- package/src/adapters/kiro.ts +45 -42
- package/src/adapters/openai-chat.ts +5 -2
- package/src/adapters/openai-responses.ts +5 -1
- package/src/cli/dispatch.ts +6 -3
- package/src/cli/export-command.ts +19 -7
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +1 -0
- package/src/cli/registry.ts +2 -2
- package/src/clients/config-export.ts +165 -3
- package/src/generated/compatibility-version.json +59 -31
- package/src/integrations/config-io.ts +119 -1
- package/src/integrations/omp-yaml-source.ts +232 -99
- package/src/integrations/registry.ts +14 -0
- package/src/integrations/serialize.ts +80 -1
- package/src/integrations/state.ts +38 -6
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +152 -19
- package/src/lab/automation/orchestrator.ts +19 -0
- package/src/lib/lab-activation.ts +161 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/shadow-call.ts +6 -14
- package/src/lib/translator-budget.ts +34 -0
- package/src/providers/antigravity-models.ts +65 -10
- package/src/routing/compatibility/assemble.ts +21 -107
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/server/index.ts +8 -17
- package/src/server/lifecycle.ts +5 -3
- package/src/server/management/integration-routes.ts +21 -14
- package/src/server/management/routing-profile-routes.ts +9 -1
- package/src/server/management-api.ts +37 -6
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/responses/core.ts +20 -21
- package/src/types.ts +15 -5
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-DOKr6RBR.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-DUCH59lJ.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
package/src/adapters/base.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface AdapterRequest {
|
|
|
46
46
|
method: string;
|
|
47
47
|
headers: Record<string, string>;
|
|
48
48
|
body: string;
|
|
49
|
+
/** Custom-tool names actually lowered to upstream function calls while building this request. */
|
|
50
|
+
convertedRoutedCustomToolNames?: ReadonlySet<string>;
|
|
49
51
|
/** Releases observation of a serialized request body after its final fetch attempt settles. */
|
|
50
52
|
releaseBodyObservation?: () => void;
|
|
51
53
|
/** Exact reasoning parameter emitted by the adapter, for request-log diagnostics only. */
|
|
@@ -522,7 +522,15 @@ function refreshReplaySessionCandidate(key: string, entry: ReplayEntry): void {
|
|
|
522
522
|
}
|
|
523
523
|
|
|
524
524
|
function deleteExpiredReplaySessions(now: number): void {
|
|
525
|
-
|
|
525
|
+
let deleted = false;
|
|
526
|
+
for (const [key, entry] of replayCache) {
|
|
527
|
+
if (entry.expiresAtMs > now) continue;
|
|
528
|
+
deleteReplaySession(key);
|
|
529
|
+
deleted = true;
|
|
530
|
+
}
|
|
531
|
+
// Expiry is a durable mutation too: rewrite the snapshot so opaque thought
|
|
532
|
+
// signatures do not remain at rest after their in-memory TTL has elapsed.
|
|
533
|
+
if (deleted) markReplayDirty();
|
|
526
534
|
}
|
|
527
535
|
|
|
528
536
|
/**
|
|
@@ -75,6 +75,14 @@ export class KiroThinkingParser {
|
|
|
75
75
|
return [];
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/** Release any partial tag/content carry when the owning stream stops early. */
|
|
79
|
+
dispose(): void {
|
|
80
|
+
this.replaceCarry("preBuffer", "");
|
|
81
|
+
this.replaceCarry("thinkingBuffer", "");
|
|
82
|
+
this.closeTag = "";
|
|
83
|
+
this.state = "streaming";
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
private drainThinking(): AdapterEvent[] {
|
|
79
87
|
const close = this.closeTag;
|
|
80
88
|
const idx = this.thinkingBuffer.indexOf(close);
|
package/src/adapters/kiro.ts
CHANGED
|
@@ -19,6 +19,8 @@ import { createKiroToolNameRegistry, fallbackToolUseId, fingerprint, invocationI
|
|
|
19
19
|
import { namespacedToolName } from "../types";
|
|
20
20
|
import {
|
|
21
21
|
isTranslatorBudgetExceededError,
|
|
22
|
+
releaseTranslatedEvent,
|
|
23
|
+
retainTranslatedEvent,
|
|
22
24
|
type TranslatorBudget,
|
|
23
25
|
} from "../lib/translator-budget";
|
|
24
26
|
import type {
|
|
@@ -636,8 +638,8 @@ export function buildKiroPayload(
|
|
|
636
638
|
|
|
637
639
|
// Stream parsing (shared by parseStream + parseResponse)
|
|
638
640
|
// CodeWhisperer GenerateAssistantResponse ALWAYS returns an AWS eventstream body (there is no
|
|
639
|
-
// non-streaming mode), so
|
|
640
|
-
//
|
|
641
|
+
// non-streaming wire mode), so the streaming bridge and non-streaming Responses path decode the
|
|
642
|
+
// same way — parseResponse just collects what parseStream yields.
|
|
641
643
|
interface KiroAttemptParseResult {
|
|
642
644
|
terminal?: AdapterEvent;
|
|
643
645
|
needsFallback?: boolean;
|
|
@@ -859,16 +861,12 @@ async function* parseKiroAttempt(
|
|
|
859
861
|
);
|
|
860
862
|
let handedOff = false;
|
|
861
863
|
try {
|
|
862
|
-
|
|
863
|
-
while (!next.done) {
|
|
864
|
-
yield next.value;
|
|
865
|
-
next = await attempt.next();
|
|
866
|
-
}
|
|
864
|
+
const result = yield* attempt;
|
|
867
865
|
for (const event of deferred.splice(0)) {
|
|
868
866
|
try { yield event; } finally { retention.releaseEvent(event); }
|
|
869
867
|
}
|
|
870
868
|
handedOff = true;
|
|
871
|
-
return { ...
|
|
869
|
+
return { ...result, releaseRetained: () => retention.releaseAll() };
|
|
872
870
|
} finally {
|
|
873
871
|
if (!handedOff) retention.releaseAll();
|
|
874
872
|
}
|
|
@@ -897,6 +895,12 @@ async function* parseKiroAttemptEvents(
|
|
|
897
895
|
}
|
|
898
896
|
|
|
899
897
|
let open: { id: string; name: string; chunks: string[]; completion: boolean } | null = null;
|
|
898
|
+
let openCallId: string | undefined;
|
|
899
|
+
const closeOpenCall = () => {
|
|
900
|
+
if (!openCallId) return;
|
|
901
|
+
budget.closeCall(openCallId);
|
|
902
|
+
openCallId = undefined;
|
|
903
|
+
};
|
|
900
904
|
let outputChars = "";
|
|
901
905
|
let outputCharsBytes = 0;
|
|
902
906
|
let contextUsagePercentage: number | undefined;
|
|
@@ -1109,7 +1113,7 @@ async function* parseKiroAttemptEvents(
|
|
|
1109
1113
|
if (!open) return { events: [] };
|
|
1110
1114
|
const tool = open;
|
|
1111
1115
|
open = null;
|
|
1112
|
-
|
|
1116
|
+
closeOpenCall();
|
|
1113
1117
|
const input = tool.chunks.join("");
|
|
1114
1118
|
if (!isCompleteKiroToolInput(input)) {
|
|
1115
1119
|
return { events: [], terminal: protocolTerminal(kiroTruncationErrorMessage("incomplete tool input JSON"), tool.completion) };
|
|
@@ -1221,11 +1225,12 @@ async function* parseKiroAttemptEvents(
|
|
|
1221
1225
|
if (started.terminal) return { assistantText, sawReasoning, terminal: started.terminal };
|
|
1222
1226
|
open = started.tool!;
|
|
1223
1227
|
budget.openCall(open.id);
|
|
1228
|
+
openCallId = open.id;
|
|
1224
1229
|
} else if (
|
|
1225
1230
|
(ev.toolUseId && ev.toolUseId !== open.id)
|
|
1226
1231
|
|| (ev.name && open.name !== "unknown" && ev.name !== open.name)
|
|
1227
1232
|
) {
|
|
1228
|
-
|
|
1233
|
+
closeOpenCall();
|
|
1229
1234
|
open = null;
|
|
1230
1235
|
return { assistantText, sawReasoning, terminal: protocolTerminal(kiroTruncationErrorMessage("tool input changed identity before stop")) };
|
|
1231
1236
|
}
|
|
@@ -1489,7 +1494,7 @@ async function* parseKiroAttemptEvents(
|
|
|
1489
1494
|
};
|
|
1490
1495
|
} catch (err) {
|
|
1491
1496
|
if (isTranslatorBudgetExceededError(err)) {
|
|
1492
|
-
|
|
1497
|
+
closeOpenCall();
|
|
1493
1498
|
return {
|
|
1494
1499
|
assistantText,
|
|
1495
1500
|
sawReasoning,
|
|
@@ -1531,6 +1536,9 @@ async function* parseKiroAttemptEvents(
|
|
|
1531
1536
|
usage: usage(),
|
|
1532
1537
|
},
|
|
1533
1538
|
};
|
|
1539
|
+
} finally {
|
|
1540
|
+
thinking.dispose();
|
|
1541
|
+
closeOpenCall();
|
|
1534
1542
|
}
|
|
1535
1543
|
}
|
|
1536
1544
|
|
|
@@ -1547,7 +1555,7 @@ export async function* parseKiroStream(
|
|
|
1547
1555
|
contextInputEstimate?: number,
|
|
1548
1556
|
): AsyncGenerator<AdapterEvent> {
|
|
1549
1557
|
const contextWindowState: KiroContextWindowState = { value: contextWindow };
|
|
1550
|
-
const
|
|
1558
|
+
const firstResult = yield* parseKiroAttempt(
|
|
1551
1559
|
response,
|
|
1552
1560
|
budget,
|
|
1553
1561
|
completionMode,
|
|
@@ -1559,12 +1567,6 @@ export async function* parseKiroStream(
|
|
|
1559
1567
|
contextInputEstimate,
|
|
1560
1568
|
false,
|
|
1561
1569
|
);
|
|
1562
|
-
let firstNext = await first.next();
|
|
1563
|
-
while (!firstNext.done) {
|
|
1564
|
-
yield firstNext.value;
|
|
1565
|
-
firstNext = await first.next();
|
|
1566
|
-
}
|
|
1567
|
-
const firstResult = firstNext.value;
|
|
1568
1570
|
try {
|
|
1569
1571
|
if (!firstResult.needsFallback) {
|
|
1570
1572
|
if (firstResult.terminal) yield firstResult.terminal;
|
|
@@ -1642,7 +1644,7 @@ export async function* parseKiroStream(
|
|
|
1642
1644
|
return;
|
|
1643
1645
|
}
|
|
1644
1646
|
|
|
1645
|
-
const
|
|
1647
|
+
const secondResult = yield* parseKiroAttempt(
|
|
1646
1648
|
fallback.response,
|
|
1647
1649
|
budget,
|
|
1648
1650
|
"text_fallback",
|
|
@@ -1656,12 +1658,6 @@ export async function* parseKiroStream(
|
|
|
1656
1658
|
// A zero-output transport failure here must stay non-retryable to avoid duplicating that text.
|
|
1657
1659
|
priorEmittedOutput,
|
|
1658
1660
|
);
|
|
1659
|
-
let secondNext = await second.next();
|
|
1660
|
-
while (!secondNext.done) {
|
|
1661
|
-
yield secondNext.value;
|
|
1662
|
-
secondNext = await second.next();
|
|
1663
|
-
}
|
|
1664
|
-
const secondResult = secondNext.value;
|
|
1665
1661
|
try {
|
|
1666
1662
|
if (!secondResult.terminal) {
|
|
1667
1663
|
yield retryableKiroIncomplete(
|
|
@@ -1909,25 +1905,32 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
1909
1905
|
return safeKiroHttpErrorMessage(status, headers, payloadText);
|
|
1910
1906
|
},
|
|
1911
1907
|
|
|
1912
|
-
//
|
|
1913
|
-
//
|
|
1914
|
-
//
|
|
1915
|
-
// tool failed with "web-search sidecar requires a non-streaming adapter" (kiro-only).
|
|
1908
|
+
// Kiro always returns an event stream, including for non-streaming Responses requests. Drain
|
|
1909
|
+
// the decoder into a budget-owned batch so an upstream stream cannot grow this array without
|
|
1910
|
+
// bound while the caller waits for the complete JSON response.
|
|
1916
1911
|
async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
|
|
1917
1912
|
const events: AdapterEvent[] = [];
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1913
|
+
try {
|
|
1914
|
+
for await (const e of parseKiroStream(
|
|
1915
|
+
response,
|
|
1916
|
+
budget,
|
|
1917
|
+
modelId,
|
|
1918
|
+
inputTokens,
|
|
1919
|
+
contextWindow,
|
|
1920
|
+
toolNameMap,
|
|
1921
|
+
conversationId,
|
|
1922
|
+
completionMode,
|
|
1923
|
+
completionMode === "required" ? fallbackFactory : undefined,
|
|
1924
|
+
contextInputEstimate,
|
|
1925
|
+
)) {
|
|
1926
|
+
retainTranslatedEvent(e, budget, events.at(-1));
|
|
1927
|
+
events.push(e);
|
|
1928
|
+
}
|
|
1929
|
+
return events;
|
|
1930
|
+
} catch (error) {
|
|
1931
|
+
for (const event of events) releaseTranslatedEvent(event, budget);
|
|
1932
|
+
throw error;
|
|
1933
|
+
}
|
|
1931
1934
|
},
|
|
1932
1935
|
};
|
|
1933
1936
|
}
|
|
@@ -968,8 +968,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
968
968
|
if (provider.parallelToolCalls === false) {
|
|
969
969
|
// NIM documents the Boolean defaulting to false and kimi rejects true; pin the
|
|
970
970
|
// wire bit so Codex cannot opt in via request.options. Other opted-out providers
|
|
971
|
-
// omit the field so strict OpenAI-compatible hosts never see an
|
|
972
|
-
|
|
971
|
+
// omit the field by default so strict OpenAI-compatible hosts never see an
|
|
972
|
+
// unsupported knob, but a self-hosted gateway that DOES honor the field and keeps
|
|
973
|
+
// emitting parallel calls without it can opt in via pinParallelToolCallsFalse.
|
|
974
|
+
if (provider.baseUrl === "https://integrate.api.nvidia.com/v1"
|
|
975
|
+
|| provider.pinParallelToolCallsFalse === true) {
|
|
973
976
|
body.parallel_tool_calls = false;
|
|
974
977
|
}
|
|
975
978
|
} else if (provider.parallelToolCalls === true) {
|
|
@@ -1361,6 +1361,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1361
1361
|
}
|
|
1362
1362
|
|
|
1363
1363
|
const forward = provider.authMode === "forward";
|
|
1364
|
+
let convertedRoutedCustomToolNames: Set<string> | undefined;
|
|
1364
1365
|
const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
|
|
1365
1366
|
let outBody = stripPreviousResponseId(
|
|
1366
1367
|
parsed._rawBody,
|
|
@@ -1408,7 +1409,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1408
1409
|
outBody = promoteClientLoadedTools(outBody);
|
|
1409
1410
|
}
|
|
1410
1411
|
if (provider.authMode !== "forward") {
|
|
1411
|
-
|
|
1412
|
+
const rewritten = rewriteRoutedCustomToolsForUpstream(outBody);
|
|
1413
|
+
outBody = rewritten.body;
|
|
1414
|
+
convertedRoutedCustomToolNames = rewritten.names;
|
|
1412
1415
|
}
|
|
1413
1416
|
const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody), { preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true })))))));
|
|
1414
1417
|
const body = JSON.stringify(stripDisabledReasoningSummaries(
|
|
@@ -1426,6 +1429,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1426
1429
|
headers,
|
|
1427
1430
|
body,
|
|
1428
1431
|
releaseBodyObservation,
|
|
1432
|
+
...(convertedRoutedCustomToolNames ? { convertedRoutedCustomToolNames } : {}),
|
|
1429
1433
|
};
|
|
1430
1434
|
},
|
|
1431
1435
|
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -21,7 +21,6 @@ import { restoreNativeCodexAsync } from "../codex/inject";
|
|
|
21
21
|
import { stripGrokConfig } from "../grok/inject";
|
|
22
22
|
import { afterCatalogWriteHandleAppServers } from "../codex/app-server-processes";
|
|
23
23
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
24
|
-
import { serviceCommand } from "../service";
|
|
25
24
|
|
|
26
25
|
export interface CliDispatchDeps {
|
|
27
26
|
args: string[];
|
|
@@ -45,6 +44,7 @@ export interface CliDispatchDeps {
|
|
|
45
44
|
handleStatus: () => Promise<void>;
|
|
46
45
|
handleRecoverHistory: () => Promise<void>;
|
|
47
46
|
handleReady: (args: ReadyArgs) => Promise<number>;
|
|
47
|
+
serviceCommand: (...args: string[]) => Promise<void>;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
type CommandRunner = (deps: CliDispatchDeps) => Promise<number>;
|
|
@@ -261,8 +261,11 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
261
261
|
return 0;
|
|
262
262
|
},
|
|
263
263
|
service: async deps => {
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
process.exitCode = 0;
|
|
265
|
+
await deps.serviceCommand(...deps.args.slice(1));
|
|
266
|
+
// serviceCommand uses process.exitCode for recoverable install/stop failures
|
|
267
|
+
// that must finish cleanup before the single top-level process.exit runs.
|
|
268
|
+
return Number(process.exitCode ?? 0);
|
|
266
269
|
},
|
|
267
270
|
tray: async deps => {
|
|
268
271
|
const { windowsTrayCommand } = await import("../tray/windows");
|
|
@@ -62,7 +62,11 @@ export interface ExportCommandDeps extends RuntimeApiDeps {
|
|
|
62
62
|
* `/api/models` row plus the modality list Pi consumes. The launcher's row type predates
|
|
63
63
|
* the Pi exporter and stops at the fields OpenCode needs.
|
|
64
64
|
*/
|
|
65
|
-
type ExportProxyModelRow = OpencodeProxyModelRow & {
|
|
65
|
+
type ExportProxyModelRow = OpencodeProxyModelRow & {
|
|
66
|
+
inputModalities?: string[];
|
|
67
|
+
reasoningEfforts?: string[];
|
|
68
|
+
defaultReasoningEffort?: string;
|
|
69
|
+
};
|
|
66
70
|
|
|
67
71
|
/** Same authoritativeness rule the serializers apply, for the degraded-count line. */
|
|
68
72
|
function hasContextLimit(model: ExportModel): boolean {
|
|
@@ -83,12 +87,21 @@ export function exportModelsFromProxyRows(
|
|
|
83
87
|
rows: readonly ExportProxyModelRow[],
|
|
84
88
|
config: OcxConfig,
|
|
85
89
|
): ExportModel[] {
|
|
86
|
-
const
|
|
90
|
+
const metadata = new Map<string, Pick<ExportModel, "inputModalities" | "reasoningEfforts" | "defaultReasoningEffort">>();
|
|
87
91
|
for (const row of rows) {
|
|
88
92
|
const namespaced = row.namespaced?.trim();
|
|
89
|
-
if (namespaced
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
if (!namespaced || metadata.has(namespaced)) continue;
|
|
94
|
+
metadata.set(namespaced, {
|
|
95
|
+
...(Array.isArray(row.inputModalities) && row.inputModalities.length > 0
|
|
96
|
+
? { inputModalities: [...row.inputModalities] }
|
|
97
|
+
: {}),
|
|
98
|
+
...(Array.isArray(row.reasoningEfforts) && row.reasoningEfforts.length > 0
|
|
99
|
+
? { reasoningEfforts: [...row.reasoningEfforts] }
|
|
100
|
+
: {}),
|
|
101
|
+
...(typeof row.defaultReasoningEffort === "string" && row.defaultReasoningEffort.length > 0
|
|
102
|
+
? { defaultReasoningEffort: row.defaultReasoningEffort }
|
|
103
|
+
: {}),
|
|
104
|
+
});
|
|
92
105
|
}
|
|
93
106
|
return opencodeCatalogFromProxyRows(rows, config).map(entry => {
|
|
94
107
|
const model: ExportModel = {
|
|
@@ -99,8 +112,7 @@ export function exportModelsFromProxyRows(
|
|
|
99
112
|
if (entry.native) model.native = true;
|
|
100
113
|
if (entry.displayName) model.displayName = entry.displayName;
|
|
101
114
|
if (entry.contextWindow !== undefined) model.contextWindow = entry.contextWindow;
|
|
102
|
-
|
|
103
|
-
if (input) model.inputModalities = input;
|
|
115
|
+
Object.assign(model, metadata.get(entry.namespaced));
|
|
104
116
|
return model;
|
|
105
117
|
});
|
|
106
118
|
}
|
package/src/cli/help.ts
CHANGED
|
@@ -58,7 +58,7 @@ Usage:
|
|
|
58
58
|
ocx memory [--json] Alias of ocx observe memory
|
|
59
59
|
ocx api-key <sub> Alias of ocx access key
|
|
60
60
|
ocx access <sub> External API keys and endpoint information
|
|
61
|
-
ocx export --client <id> Print a client config wired to the running proxy (
|
|
61
|
+
ocx export --client <id> Print a client config wired to the running proxy (8 clients)
|
|
62
62
|
ocx integration client <sub> Enable, disable, inspect or roll back a client integration
|
|
63
63
|
ocx grok <sub> Grok Build model selection and apply
|
|
64
64
|
ocx system <sub> Runtime settings, startup, sync, and updates
|
package/src/cli/index.ts
CHANGED
package/src/cli/registry.ts
CHANGED
|
@@ -216,8 +216,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
216
216
|
{ name: "api-key", usage: "ocx api-key <list|create|remove> ...", summary: "Alias of ocx access key." },
|
|
217
217
|
{
|
|
218
218
|
name: "export",
|
|
219
|
-
usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae> [--json] [--out <path>] [--force]",
|
|
220
|
-
summary: "Print a client config (
|
|
219
|
+
usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh> [--json] [--out <path>] [--force]",
|
|
220
|
+
summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code, DeepSeek Harness) wired to the running proxy.",
|
|
221
221
|
details: [
|
|
222
222
|
"--json prints the generated document as JSON on stdout; use --out for the client's native format.",
|
|
223
223
|
"--out <path> writes the native config there and refuses to replace an existing file without --force.",
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { existsSync } from "node:fs";
|
|
24
|
-
import { isAbsolute, join } from "node:path";
|
|
24
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
25
25
|
import { shouldInjectApiAuthHeader } from "../codex/inject";
|
|
26
26
|
import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
|
|
27
|
+
import { providerCodexAccountMode } from "../providers/registry";
|
|
27
28
|
import { probeHostname } from "../server/proxy-liveness";
|
|
28
29
|
import type { OcxConfig } from "../types";
|
|
29
30
|
|
|
@@ -376,6 +377,28 @@ export function gajaeConfigPath(env: OpencodeLaunchEnv = process.env, home: stri
|
|
|
376
377
|
return join(gajaeHomeDir(env, home), "agent", "models.yml");
|
|
377
378
|
}
|
|
378
379
|
|
|
380
|
+
/** DSH_HOME uses the raw nonblank value; trimming it would name a different path. */
|
|
381
|
+
export function dshHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
382
|
+
const raw = env.DSH_HOME;
|
|
383
|
+
if (raw === undefined || raw.trim().length === 0) return join(home, ".dsh");
|
|
384
|
+
if (raw === "~") return home;
|
|
385
|
+
if (raw.startsWith("~/") || raw.startsWith("~\\")) return join(home, raw.slice(2));
|
|
386
|
+
if (!isAbsolute(raw)) {
|
|
387
|
+
throw new ClientPathError(
|
|
388
|
+
`DSH_HOME must be an absolute path or start with ~; "${raw}" depends on the working directory, `
|
|
389
|
+
+ "so opencodex and DSH would disagree about which settings file it names.",
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
// DSH calls node:path.resolve after tilde expansion. Preserve the raw value
|
|
393
|
+
// for the decision above, then normalize the absolute spelling the same way
|
|
394
|
+
// so both processes bind ownership and locks to one path string.
|
|
395
|
+
return resolve(raw);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function dshConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
399
|
+
return join(dshHomeDir(env, home), "settings.yaml");
|
|
400
|
+
}
|
|
401
|
+
|
|
379
402
|
/**
|
|
380
403
|
* One proxy-routed model destined for a client config. Deliberately narrower than
|
|
381
404
|
* `CatalogModel` so a serializer cannot reach for a field that does not survive the
|
|
@@ -414,7 +437,8 @@ export type ExportClientId =
|
|
|
414
437
|
| "hermes"
|
|
415
438
|
| "openclaw"
|
|
416
439
|
| "kimi"
|
|
417
|
-
| "gajae"
|
|
440
|
+
| "gajae"
|
|
441
|
+
| "dsh";
|
|
418
442
|
|
|
419
443
|
export interface ExportClientSpec {
|
|
420
444
|
id: ExportClientId;
|
|
@@ -463,7 +487,8 @@ export interface ExportClientSpec {
|
|
|
463
487
|
*/
|
|
464
488
|
function authoritativeContextWindow(contextWindow: number | undefined): number | undefined {
|
|
465
489
|
if (typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) {
|
|
466
|
-
|
|
490
|
+
const integer = Math.floor(contextWindow);
|
|
491
|
+
return integer > 0 ? integer : undefined;
|
|
467
492
|
}
|
|
468
493
|
return undefined;
|
|
469
494
|
}
|
|
@@ -522,6 +547,18 @@ function inputModalitiesForClient(
|
|
|
522
547
|
return kept.length > 0 ? kept : null;
|
|
523
548
|
}
|
|
524
549
|
|
|
550
|
+
/** DSH rc.6 accepts text/image; unknown values degrade to text, while audio-only cannot be represented. */
|
|
551
|
+
function dshInputModalities(modalities: readonly string[] | undefined): string[] | null {
|
|
552
|
+
const declared = modalities ?? [];
|
|
553
|
+
if (declared.length === 0) return ["text"];
|
|
554
|
+
const kept: string[] = [];
|
|
555
|
+
for (const value of declared) {
|
|
556
|
+
if ((value === "text" || value === "image") && !kept.includes(value)) kept.push(value);
|
|
557
|
+
}
|
|
558
|
+
if (kept.length > 0) return kept;
|
|
559
|
+
return declared.every(value => value === "audio") ? null : ["text"];
|
|
560
|
+
}
|
|
561
|
+
|
|
525
562
|
/**
|
|
526
563
|
* Label shared by every client: `"<displayName|id> (<native|provider|routed>)"`. The
|
|
527
564
|
* provider suffix is what makes two same-named models from different upstreams
|
|
@@ -770,6 +807,31 @@ export interface GajaeGeneratedConfig {
|
|
|
770
807
|
providers: Record<string, GajaeProviderBlock>;
|
|
771
808
|
}
|
|
772
809
|
|
|
810
|
+
export type DshReasoningEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
|
811
|
+
export type DshWireReasoningEffort = DshReasoningEffort | "ultra";
|
|
812
|
+
|
|
813
|
+
export interface DshModelEntry {
|
|
814
|
+
id: string;
|
|
815
|
+
name: string;
|
|
816
|
+
input: string[];
|
|
817
|
+
contextWindow?: number;
|
|
818
|
+
reasoningEfforts?: Partial<Record<DshReasoningEffort, DshWireReasoningEffort>>;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
export interface DshProviderBlock {
|
|
822
|
+
displayName: "OpenCodex";
|
|
823
|
+
api: "openai-responses";
|
|
824
|
+
baseURL: string;
|
|
825
|
+
headers: { Authorization: "Bearer ocx_data_dsh" };
|
|
826
|
+
models: DshModelEntry[];
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export interface DshGeneratedConfig {
|
|
830
|
+
"llm-pi-ai": {
|
|
831
|
+
providers: Record<string, DshProviderBlock>;
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
773
835
|
/**
|
|
774
836
|
* Pi's `~/.pi/agent/models.json` shape. `models` is an ARRAY (identity lives in `id`),
|
|
775
837
|
* unlike OpenCode's keyed object.
|
|
@@ -975,6 +1037,84 @@ function buildGajaeClientConfig(ctx: ExportContext): GajaeGeneratedConfig {
|
|
|
975
1037
|
};
|
|
976
1038
|
}
|
|
977
1039
|
|
|
1040
|
+
const DSH_EFFORT_ORDER: readonly DshReasoningEffort[] = ["low", "medium", "high", "xhigh", "max"];
|
|
1041
|
+
|
|
1042
|
+
function dshReasoningEfforts(model: ExportModel): DshModelEntry["reasoningEfforts"] {
|
|
1043
|
+
const offered = new Set<string>();
|
|
1044
|
+
for (const raw of model.reasoningEfforts ?? []) {
|
|
1045
|
+
const effort = raw.trim().toLowerCase();
|
|
1046
|
+
if (effort === "ultra" || DSH_EFFORT_ORDER.includes(effort as DshReasoningEffort)) offered.add(effort);
|
|
1047
|
+
}
|
|
1048
|
+
if (offered.size === 0) return undefined;
|
|
1049
|
+
const entries: Array<[DshReasoningEffort, DshWireReasoningEffort]> = [];
|
|
1050
|
+
for (const effort of DSH_EFFORT_ORDER) {
|
|
1051
|
+
if (effort !== "max") {
|
|
1052
|
+
if (offered.has(effort)) entries.push([effort, effort]);
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
// DSH's key is the selectable level; the value is what it sends on the
|
|
1056
|
+
// wire. Preserve OpenCodex's `ultra` spelling when that is the only
|
|
1057
|
+
// highest effort, exactly like the rc.6 `max: ultra` contract.
|
|
1058
|
+
if (offered.has("max")) entries.push(["max", "max"]);
|
|
1059
|
+
else if (offered.has("ultra")) entries.push(["max", "ultra"]);
|
|
1060
|
+
}
|
|
1061
|
+
return Object.fromEntries(entries);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function isKnownSafeDshCombo(model: ExportModel, config: OcxConfig): boolean {
|
|
1065
|
+
const combos = (config as { combos?: unknown }).combos;
|
|
1066
|
+
if (typeof combos !== "object" || combos === null || Array.isArray(combos)) return false;
|
|
1067
|
+
const combo = (combos as Record<string, unknown>)[model.id];
|
|
1068
|
+
if (typeof combo !== "object" || combo === null || Array.isArray(combo)) return false;
|
|
1069
|
+
const targets = (combo as { targets?: unknown }).targets;
|
|
1070
|
+
if (!Array.isArray(targets) || targets.length === 0) return false;
|
|
1071
|
+
return targets.every(target => {
|
|
1072
|
+
if (typeof target !== "object" || target === null || Array.isArray(target)) return false;
|
|
1073
|
+
const provider = (target as { provider?: unknown }).provider;
|
|
1074
|
+
const modelId = (target as { model?: unknown }).model;
|
|
1075
|
+
return typeof provider === "string"
|
|
1076
|
+
&& provider.length > 0
|
|
1077
|
+
&& provider === provider.trim()
|
|
1078
|
+
&& provider !== "openai"
|
|
1079
|
+
&& typeof modelId === "string"
|
|
1080
|
+
&& modelId.length > 0
|
|
1081
|
+
&& modelId === modelId.trim();
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function buildDshClientConfig(ctx: ExportContext): DshGeneratedConfig {
|
|
1086
|
+
const direct = providerCodexAccountMode("openai", ctx.config?.providers?.openai) === "direct";
|
|
1087
|
+
const models: DshModelEntry[] = [];
|
|
1088
|
+
for (const model of normalizeExportModels(ctx.models)) {
|
|
1089
|
+
if (direct && (model.native === true || model.provider === "openai")) continue;
|
|
1090
|
+
if (direct && model.provider === "combo" && (!ctx.config || !isKnownSafeDshCombo(model, ctx.config))) continue;
|
|
1091
|
+
const input = dshInputModalities(model.inputModalities);
|
|
1092
|
+
if (input === null) continue;
|
|
1093
|
+
const contextWindow = authoritativeContextWindow(model.contextWindow);
|
|
1094
|
+
const reasoningEfforts = dshReasoningEfforts(model);
|
|
1095
|
+
models.push({
|
|
1096
|
+
id: model.namespaced,
|
|
1097
|
+
name: exportModelLabel(model),
|
|
1098
|
+
input,
|
|
1099
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
1100
|
+
...(reasoningEfforts ? { reasoningEfforts } : {}),
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
"llm-pi-ai": {
|
|
1105
|
+
providers: {
|
|
1106
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
1107
|
+
displayName: "OpenCodex",
|
|
1108
|
+
api: "openai-responses",
|
|
1109
|
+
baseURL: ctx.baseUrl,
|
|
1110
|
+
headers: { Authorization: "Bearer ocx_data_dsh" },
|
|
1111
|
+
models,
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
},
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
978
1118
|
/**
|
|
979
1119
|
* Per-client model counts, read back off the SERIALIZED document rather than
|
|
980
1120
|
* recomputed from the input rows: `modelsWithoutLimits` drives a GUI line about
|
|
@@ -1019,6 +1159,11 @@ function summarizeGajae(document: unknown): { modelCount: number; modelsWithoutL
|
|
|
1019
1159
|
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
|
|
1020
1160
|
}
|
|
1021
1161
|
|
|
1162
|
+
function summarizeDsh(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
1163
|
+
const models = (document as DshGeneratedConfig | undefined)?.["llm-pi-ai"]?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
|
|
1164
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1022
1167
|
/** One fragment at `path`, built from this client's own document. */
|
|
1023
1168
|
function singleFragment(clientId: ExportClientId, path: readonly string[], value: unknown): ManagedContribution {
|
|
1024
1169
|
return { clientId, fragments: [{ path, value }] };
|
|
@@ -1070,6 +1215,11 @@ function buildGajaeContribution(ctx: ExportContext): ManagedContribution {
|
|
|
1070
1215
|
return singleFragment("gajae", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1071
1216
|
}
|
|
1072
1217
|
|
|
1218
|
+
function buildDshContribution(ctx: ExportContext): ManagedContribution {
|
|
1219
|
+
const doc = buildDshClientConfig(ctx);
|
|
1220
|
+
return singleFragment("dsh", ["llm-pi-ai", "providers", OPENCODE_PROVIDER_ID], doc["llm-pi-ai"].providers[OPENCODE_PROVIDER_ID]);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1073
1223
|
export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
1074
1224
|
opencode: {
|
|
1075
1225
|
id: "opencode",
|
|
@@ -1167,6 +1317,18 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
|
1167
1317
|
// strict schema with no header field, so the dedicated header has nowhere to go
|
|
1168
1318
|
loopbackOnly: true,
|
|
1169
1319
|
},
|
|
1320
|
+
dsh: {
|
|
1321
|
+
id: "dsh",
|
|
1322
|
+
filename: "settings.yaml",
|
|
1323
|
+
destination: env => dshConfigPath(env),
|
|
1324
|
+
apiKeyEnv: "",
|
|
1325
|
+
exportHint: "DSH uses a non-secret loopback bearer placeholder in settings.yaml; loopback needs no key.",
|
|
1326
|
+
build: buildDshClientConfig,
|
|
1327
|
+
format: "yaml",
|
|
1328
|
+
summarize: summarizeDsh,
|
|
1329
|
+
buildContribution: buildDshContribution,
|
|
1330
|
+
loopbackOnly: true,
|
|
1331
|
+
},
|
|
1170
1332
|
};
|
|
1171
1333
|
|
|
1172
1334
|
export const EXPORT_CLIENT_IDS: readonly ExportClientId[] = Object.keys(EXPORT_CLIENTS) as ExportClientId[];
|