@bitkyc08/opencodex 2.45.0 → 2.46.0-preview.20260907
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-J96sug5C.css → index-BFgUC17B.css} +1 -1
- package/gui/dist/assets/{index-CCfD72yq.js → index-NcAVXkST.js} +19 -19
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/raycast.svg +3 -0
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +8 -3
- package/src/adapters/openai-responses.ts +7 -0
- package/src/bridge.ts +27 -7
- package/src/claude/inbound.ts +23 -5
- package/src/claude/outbound.ts +87 -19
- package/src/cli/capabilities.ts +4 -1
- package/src/cli/dispatch.ts +1 -1
- package/src/cli/doctor.ts +2 -2
- package/src/cli/export-command.ts +22 -10
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +34 -7
- package/src/cli/integrations.ts +34 -1
- package/src/cli/provider.ts +27 -15
- package/src/cli/registry.ts +2 -2
- package/src/cli/version-skew.ts +36 -3
- package/src/clients/aside-profiles.ts +8 -7
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export/raycast.ts +106 -0
- package/src/clients/config-export.ts +36 -0
- package/src/clients/model-presentation.ts +61 -0
- package/src/codex/catalog/sync.ts +44 -2
- package/src/codex/convergence.ts +7 -0
- package/src/generated/compatibility-version.json +59 -43
- package/src/generated/model-metadata.ts +1 -0
- package/src/images/loop.ts +10 -2
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +158 -25
- package/src/integrations/raycast-detect.ts +111 -0
- package/src/integrations/registry.ts +18 -0
- package/src/integrations/state.ts +82 -13
- package/src/integrations/writer.ts +46 -31
- package/src/lib/bounded-body.ts +22 -7
- package/src/oauth/anthropic-routing.ts +59 -14
- package/src/oauth/health.ts +3 -0
- package/src/providers/quota.ts +145 -9
- package/src/providers/registry.ts +31 -0
- package/src/responses/parser.ts +16 -3
- package/src/responses/reasoning-envelope.ts +3 -2
- package/src/server/grok-responses-control-frame.ts +43 -0
- package/src/server/management/config-routes.ts +6 -2
- package/src/server/management/integration-routes.ts +29 -2
- package/src/server/management/model-routes.ts +9 -1
- package/src/server/request-decompress.ts +34 -8
- package/src/server/responses/agent-task-recovery-cache.ts +43 -14
- package/src/server/responses/agent-task-recovery.ts +28 -16
- package/src/server/responses/core.ts +39 -6
- package/src/web-search/loop.ts +10 -2
package/src/cli/integrations.ts
CHANGED
|
@@ -161,6 +161,39 @@ export async function handleGrokCommand(argv: string[], deps: RuntimeApiDeps = {
|
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/** The Raycast-only block the single-client route adds; see IntegrationStateEnvelope. */
|
|
165
|
+
interface RaycastStatusBlock {
|
|
166
|
+
plan: string;
|
|
167
|
+
aiDirPresent: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function raycastBlock(result: unknown): RaycastStatusBlock | null {
|
|
171
|
+
if (!result || typeof result !== "object") return null;
|
|
172
|
+
const block = (result as { raycast?: unknown }).raycast;
|
|
173
|
+
if (!block || typeof block !== "object") return null;
|
|
174
|
+
const { plan, aiDirPresent } = block as Partial<RaycastStatusBlock>;
|
|
175
|
+
return typeof plan === "string" && typeof aiDirPresent === "boolean" ? { plan, aiDirPresent } : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Text view of one client's status.
|
|
180
|
+
*
|
|
181
|
+
* Raycast carries an extra block, and the generic summary would print it as
|
|
182
|
+
* three dotted keys. A `current` file that Raycast ignores for want of a Pro
|
|
183
|
+
* subscription is the one fact this view must not bury, so `plan` gets its own
|
|
184
|
+
* line and a missing `ai` folder gets the instruction that creates it.
|
|
185
|
+
*/
|
|
186
|
+
function singleClientStatusLines(result: unknown): string[] {
|
|
187
|
+
const raycast = raycastBlock(result);
|
|
188
|
+
if (!raycast) return summaryLines(result);
|
|
189
|
+
const rest = Object.fromEntries(Object.entries(result as Record<string, unknown>).filter(([key]) => key !== "raycast"));
|
|
190
|
+
const lines = [...summaryLines(rest), `plan: ${raycast.plan}`];
|
|
191
|
+
if (!raycast.aiDirPresent) {
|
|
192
|
+
lines.push('Open Raycast → Settings → AI → "Reveal Providers Config" once so the ai folder exists.');
|
|
193
|
+
}
|
|
194
|
+
return lines;
|
|
195
|
+
}
|
|
196
|
+
|
|
164
197
|
/**
|
|
165
198
|
* The headless half of the client-integration toggle.
|
|
166
199
|
*
|
|
@@ -197,7 +230,7 @@ export async function handleClientIntegrationCommand(
|
|
|
197
230
|
: [String((result as { error?: string }).error ?? "No Aside profiles found.")]
|
|
198
231
|
: rows
|
|
199
232
|
? rows.map(row => `${String(row.clientId)}: ${String(row.state)}${row.installed ? "" : " (not installed)"}`)
|
|
200
|
-
:
|
|
233
|
+
: singleClientStatusLines(result));
|
|
201
234
|
return;
|
|
202
235
|
}
|
|
203
236
|
|
package/src/cli/provider.ts
CHANGED
|
@@ -79,26 +79,37 @@ function validateAndSave(config: ReturnType<typeof loadConfig>): void {
|
|
|
79
79
|
|
|
80
80
|
function handleList(args: string[]): void {
|
|
81
81
|
const wantsJson = consumeFlag(args, "--json");
|
|
82
|
-
|
|
82
|
+
const wantsJsonl = consumeFlag(args, "--jsonl");
|
|
83
|
+
rejectUnknownArgs(args, "Usage: ocx provider list [--json|--jsonl]");
|
|
84
|
+
|
|
85
|
+
if (wantsJson && wantsJsonl) {
|
|
86
|
+
console.error("Use only one of --json or --jsonl.");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
83
89
|
|
|
84
90
|
const config = loadConfig();
|
|
85
91
|
const configured = Object.keys(config.providers);
|
|
92
|
+
const entries = configured.map(name => {
|
|
93
|
+
const prov = config.providers[name];
|
|
94
|
+
const registryEntry = getProviderRegistryEntry(name);
|
|
95
|
+
return {
|
|
96
|
+
name,
|
|
97
|
+
adapter: prov.adapter,
|
|
98
|
+
baseUrl: prov.baseUrl,
|
|
99
|
+
authMode: prov.authMode ?? "key",
|
|
100
|
+
defaultModel: prov.defaultModel ?? null,
|
|
101
|
+
isDefault: name === config.defaultProvider,
|
|
102
|
+
source: registryEntry ? "registry" : "custom",
|
|
103
|
+
models: prov.models ?? [],
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (wantsJsonl) {
|
|
108
|
+
for (const entry of entries) console.log(JSON.stringify(entry));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
86
111
|
|
|
87
112
|
if (wantsJson) {
|
|
88
|
-
const entries = configured.map(name => {
|
|
89
|
-
const prov = config.providers[name];
|
|
90
|
-
const registryEntry = getProviderRegistryEntry(name);
|
|
91
|
-
return {
|
|
92
|
-
name,
|
|
93
|
-
adapter: prov.adapter,
|
|
94
|
-
baseUrl: prov.baseUrl,
|
|
95
|
-
authMode: prov.authMode ?? "key",
|
|
96
|
-
defaultModel: prov.defaultModel ?? null,
|
|
97
|
-
isDefault: name === config.defaultProvider,
|
|
98
|
-
source: registryEntry ? "registry" : "custom",
|
|
99
|
-
models: prov.models ?? [],
|
|
100
|
-
};
|
|
101
|
-
});
|
|
102
113
|
console.log(JSON.stringify({ configured: entries, registryCount: PROVIDER_REGISTRY.length }, null, 2));
|
|
103
114
|
return;
|
|
104
115
|
}
|
|
@@ -444,6 +455,7 @@ Subcommands:
|
|
|
444
455
|
|
|
445
456
|
Examples:
|
|
446
457
|
ocx provider list
|
|
458
|
+
ocx provider list --jsonl
|
|
447
459
|
ocx provider add anthropic --api-key sk-ant-...
|
|
448
460
|
ocx provider add my-ollama --adapter openai-chat --base-url http://localhost:11434/v1
|
|
449
461
|
ocx provider show anthropic --json
|
package/src/cli/registry.ts
CHANGED
|
@@ -287,8 +287,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
287
287
|
{ name: "api-key", usage: "ocx api-key <list|create|rotate|remove> ...", summary: "Alias of ocx access key." },
|
|
288
288
|
{
|
|
289
289
|
name: "export",
|
|
290
|
-
usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|prime|aside> [--json] [--out <path>] [--force]",
|
|
291
|
-
summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside) wired to the running proxy.",
|
|
290
|
+
usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|prime|aside|raycast> [--json] [--out <path>] [--force]",
|
|
291
|
+
summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast) wired to the running proxy.",
|
|
292
292
|
details: [
|
|
293
293
|
"--json prints the generated document as JSON on stdout; use --out for the client's native format.",
|
|
294
294
|
"--out <path> writes the native config there and refuses to replace an existing file without --force.",
|
package/src/cli/version-skew.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI-versus-proxy version skew (#2701).
|
|
2
|
+
* CLI-versus-proxy version skew (#2701, #3464).
|
|
3
3
|
*
|
|
4
4
|
* The reported failure: `ocx` on PATH is an older install than the running proxy, so its
|
|
5
5
|
* help describes commands the proxy does not have and its output describes a different
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* comparison instead of reimplementing it -- two diagnostics disagreeing about whether an
|
|
10
10
|
* install is stale would be worse than neither reporting it.
|
|
11
11
|
*/
|
|
12
|
+
import { parseStrictSemver, type StrictSemver } from "../lib/strict-semver";
|
|
12
13
|
|
|
13
14
|
/** Placeholder versions that mean "unknown", not "different". */
|
|
14
15
|
const PLACEHOLDERS = new Set(["unknown", "0.0.0"]);
|
|
@@ -22,6 +23,30 @@ export interface VersionSkew {
|
|
|
22
23
|
readonly warning: string | null;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** Suppressed comparisons are not confirmed matches, even when both placeholders agree. */
|
|
27
|
+
export function isConfirmedVersionMatch(skew: VersionSkew): boolean {
|
|
28
|
+
return skew.proxyVersion === skew.cliVersion && !PLACEHOLDERS.has(skew.cliVersion);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** SemVer precedence ignores build metadata; raw equality is handled separately. */
|
|
32
|
+
function compareVersions(cli: StrictSemver, proxy: StrictSemver): number {
|
|
33
|
+
for (let i = 0; i < cli.core.length; i++) {
|
|
34
|
+
if (cli.core[i]! !== proxy.core[i]!) return cli.core[i]! > proxy.core[i]! ? 1 : -1;
|
|
35
|
+
}
|
|
36
|
+
if (cli.prerelease.length === 0) return proxy.prerelease.length === 0 ? 0 : 1;
|
|
37
|
+
if (proxy.prerelease.length === 0) return -1;
|
|
38
|
+
for (let i = 0; i < Math.max(cli.prerelease.length, proxy.prerelease.length); i++) {
|
|
39
|
+
const left = cli.prerelease[i];
|
|
40
|
+
const right = proxy.prerelease[i];
|
|
41
|
+
if (left === right) continue;
|
|
42
|
+
if (left === undefined) return -1;
|
|
43
|
+
if (right === undefined) return 1;
|
|
44
|
+
if (typeof left !== typeof right) return typeof left === "bigint" ? -1 : 1;
|
|
45
|
+
return left > right ? 1 : -1;
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
25
50
|
/**
|
|
26
51
|
* Compare the running CLI against the live proxy.
|
|
27
52
|
*
|
|
@@ -36,11 +61,19 @@ export function computeVersionSkew(cliVersion: string, proxyVersion: string | un
|
|
|
36
61
|
if (proxy === null || PLACEHOLDERS.has(proxy) || PLACEHOLDERS.has(cliVersion) || proxy === cliVersion) {
|
|
37
62
|
return { cliVersion, proxyVersion: proxy, skewed: false, warning: null };
|
|
38
63
|
}
|
|
64
|
+
const cliSemver = parseStrictSemver(cliVersion);
|
|
65
|
+
const proxySemver = parseStrictSemver(proxy);
|
|
66
|
+
const order = cliSemver && proxySemver ? compareVersions(cliSemver, proxySemver) : 0;
|
|
67
|
+
const advice = order > 0
|
|
68
|
+
? "the running proxy is older than this CLI. Restart the proxy using the intended current installation. "
|
|
69
|
+
+ "For a background service, run ocx service repair (ocx service restart is an alias)."
|
|
70
|
+
: order < 0
|
|
71
|
+
? "this ocx on PATH is older than the running proxy. Upgrade the CLI or resolve PATH to the intended installation."
|
|
72
|
+
: "the versions differ, but neither can be identified as older. Check which installations the CLI and proxy use.";
|
|
39
73
|
return {
|
|
40
74
|
cliVersion,
|
|
41
75
|
proxyVersion: proxy,
|
|
42
76
|
skewed: true,
|
|
43
|
-
warning: `CLI ${cliVersion} does not match the running proxy ${proxy} —
|
|
44
|
-
+ "Its help and features describe a different build. Reinstall, or run the proxy's own binary.",
|
|
77
|
+
warning: `CLI ${cliVersion} does not match the running proxy ${proxy} — ${advice}`,
|
|
45
78
|
};
|
|
46
79
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { lstatSync, readFileSync, readlinkSync, realpathSync, statSync, type
|
|
1
|
+
import { lstatSync, readFileSync, readlinkSync, realpathSync, statSync, type BigIntStats } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import type { IntegrationIO } from "../integrations/config-io";
|
|
@@ -14,7 +14,7 @@ export interface AsideProfile {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const MAX_PROFILES = 128;
|
|
17
|
-
const MAX_MANIFEST_BYTES =
|
|
17
|
+
const MAX_MANIFEST_BYTES = 4n * 1024n * 1024n;
|
|
18
18
|
const MAX_LEAF_LINKS = 40;
|
|
19
19
|
|
|
20
20
|
function refuse(message: string): never {
|
|
@@ -30,9 +30,10 @@ function object(value: unknown): value is Record<string, unknown> {
|
|
|
30
30
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function inspect(path: string, follow = false):
|
|
33
|
+
function inspect(path: string, follow = false): BigIntStats | null {
|
|
34
34
|
try {
|
|
35
|
-
|
|
35
|
+
// File IDs can exceed Number's exact integer range; never round identities.
|
|
36
|
+
return follow ? statSync(path, { bigint: true }) : lstatSync(path, { bigint: true });
|
|
36
37
|
} catch (error) {
|
|
37
38
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
38
39
|
return refuse("a filesystem boundary could not be inspected.");
|
|
@@ -110,10 +111,10 @@ export function listAsideProfiles(env: NodeJS.ProcessEnv = process.env, home: st
|
|
|
110
111
|
return readProfiles(root);
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
type DirectoryIdentity = { path: string; dev:
|
|
114
|
+
type DirectoryIdentity = { path: string; dev: bigint; ino: bigint };
|
|
114
115
|
type Boundary = Array<DirectoryIdentity | null>;
|
|
115
116
|
|
|
116
|
-
function sameIdentity(a: Pick<
|
|
117
|
+
function sameIdentity(a: Pick<BigIntStats, "dev" | "ino">, b: Pick<BigIntStats, "dev" | "ino">): boolean {
|
|
117
118
|
return a.dev === b.dev && a.ino === b.ino;
|
|
118
119
|
}
|
|
119
120
|
|
|
@@ -162,7 +163,7 @@ function boundary(profile: AsideProfile, profiles: AsideProfile[], mutation: boo
|
|
|
162
163
|
}
|
|
163
164
|
if (absent) return identities;
|
|
164
165
|
const leaf = inspect(profile.configPath);
|
|
165
|
-
if (leaf && (leaf.isSymbolicLink() || !leaf.isFile() || leaf.nlink >
|
|
166
|
+
if (leaf && (leaf.isSymbolicLink() || !leaf.isFile() || leaf.nlink > 1n)) {
|
|
166
167
|
refuse("the model catalog is a link, shared file or non-regular file.");
|
|
167
168
|
}
|
|
168
169
|
if (leaf && canonical(profile.configPath) !== join(parent!, "models.json")) {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { exportPresentationLabel } from "../model-presentation";
|
|
2
|
+
import { OPENCODE_PROVIDER_ID } from "./constants";
|
|
3
|
+
import type { ExportContext, ManagedContribution } from "./contracts";
|
|
4
|
+
import { authoritativeContextWindow, normalizeExportModels, singleFragment } from "./model-metadata";
|
|
5
|
+
|
|
6
|
+
export interface RaycastAbility {
|
|
7
|
+
supported: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type RaycastAbilityName =
|
|
11
|
+
| "temperature"
|
|
12
|
+
| "vision"
|
|
13
|
+
| "system_message"
|
|
14
|
+
| "tools"
|
|
15
|
+
| "reasoning_effort";
|
|
16
|
+
|
|
17
|
+
export interface RaycastModelEntry {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
context?: number;
|
|
21
|
+
abilities: Record<RaycastAbilityName, RaycastAbility>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RaycastProviderEntry {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
base_url: string;
|
|
28
|
+
models: RaycastModelEntry[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RaycastGeneratedConfig {
|
|
32
|
+
providers: RaycastProviderEntry[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Raycast appends `/chat/completions` to `base_url`, so the proxy's `/v1`
|
|
37
|
+
* root is passed through unchanged. The format has no safe credential
|
|
38
|
+
* interpolation, which is why the registry exposes it only on loopback.
|
|
39
|
+
*/
|
|
40
|
+
export function buildRaycastClientConfig(ctx: ExportContext): RaycastGeneratedConfig {
|
|
41
|
+
const models: RaycastModelEntry[] = normalizeExportModels(ctx.models).map(model => {
|
|
42
|
+
const hasLadder = (model.reasoningEfforts?.length ?? 0) > 0;
|
|
43
|
+
const context = authoritativeContextWindow(model.contextWindow);
|
|
44
|
+
return {
|
|
45
|
+
id: model.namespaced,
|
|
46
|
+
name: exportPresentationLabel(model),
|
|
47
|
+
...(context !== undefined ? { context } : {}),
|
|
48
|
+
abilities: {
|
|
49
|
+
temperature: { supported: !hasLadder },
|
|
50
|
+
vision: { supported: model.inputModalities?.includes("image") ?? false },
|
|
51
|
+
system_message: { supported: true },
|
|
52
|
+
// Existing client-export convention, not a verified per-model capability:
|
|
53
|
+
// ExportModel has no authoritative tool-support field.
|
|
54
|
+
tools: { supported: true },
|
|
55
|
+
reasoning_effort: { supported: hasLadder },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
providers: [
|
|
61
|
+
{ id: OPENCODE_PROVIDER_ID, name: "OpenCodex", base_url: ctx.baseUrl, models },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
67
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function summarizeRaycast(
|
|
71
|
+
document: unknown,
|
|
72
|
+
): { modelCount: number; modelsWithoutLimits: number } {
|
|
73
|
+
const empty = { modelCount: 0, modelsWithoutLimits: 0 };
|
|
74
|
+
if (!isRecord(document) || !Array.isArray(document.providers)) return empty;
|
|
75
|
+
const providers = document.providers.filter(
|
|
76
|
+
provider => isRecord(provider) && provider.id === OPENCODE_PROVIDER_ID,
|
|
77
|
+
);
|
|
78
|
+
// An ambiguous managed provider has no meaningful summary either.
|
|
79
|
+
if (providers.length !== 1) return empty;
|
|
80
|
+
const provider: unknown = providers[0];
|
|
81
|
+
if (!isRecord(provider) || !Array.isArray(provider.models)) return empty;
|
|
82
|
+
const models = provider.models.filter((model): model is Record<string, unknown> => (
|
|
83
|
+
isRecord(model)
|
|
84
|
+
&& typeof model.id === "string" && model.id.trim().length > 0
|
|
85
|
+
&& typeof model.name === "string" && model.name.trim().length > 0
|
|
86
|
+
));
|
|
87
|
+
return {
|
|
88
|
+
modelCount: models.length,
|
|
89
|
+
modelsWithoutLimits: models.filter(model => (
|
|
90
|
+
typeof model.context !== "number" || authoritativeContextWindow(model.context) === undefined
|
|
91
|
+
)).length,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Raycast stores providers in a sequence. The stable id selector owns only
|
|
97
|
+
* OpenCodex's element, preserving user-defined providers around it.
|
|
98
|
+
*/
|
|
99
|
+
export function buildRaycastContribution(ctx: ExportContext): ManagedContribution {
|
|
100
|
+
const doc = buildRaycastClientConfig(ctx);
|
|
101
|
+
return singleFragment(
|
|
102
|
+
"raycast",
|
|
103
|
+
["providers", `[id=${OPENCODE_PROVIDER_ID}]`],
|
|
104
|
+
doc.providers[0]!,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -37,6 +37,8 @@ export type { OmpModelEntry, OmpProviderBlock, OmpGeneratedConfig } from "./conf
|
|
|
37
37
|
export type { ZcodeModelEntry, ZcodeProviderBlock, ZcodeGeneratedConfig } from "./config-export/zcode";
|
|
38
38
|
export type { DshReasoningEffort, DshWireReasoningEffort, DshModelEntry, DshProviderBlock, DshGeneratedConfig } from "./config-export/dsh";
|
|
39
39
|
export type { McodeProviderBlock, McodeModelEntry, McodeGeneratedConfig } from "./config-export/mcode";
|
|
40
|
+
export type { RaycastAbility, RaycastAbilityName, RaycastModelEntry, RaycastProviderEntry, RaycastGeneratedConfig } from "./config-export/raycast";
|
|
41
|
+
export { buildRaycastClientConfig, summarizeRaycast, buildRaycastContribution } from "./config-export/raycast";
|
|
40
42
|
|
|
41
43
|
import type { OpencodeLaunchEnv, OpencodeCatalogModel, ExportContext, PiModelEntry, ManagedContribution, ManagedFragment, ExportClientId, ExportClientSpec } from "./config-export/contracts";
|
|
42
44
|
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, GAJAE_API_KEY_ENV, OPENCODE_API_KEY_ENV, HERMES_API_KEY_ENV, OPENCLAW_API_KEY_ENV } from "./config-export/constants";
|
|
@@ -45,6 +47,7 @@ import { buildOmpClientConfig, summarizeOmp, buildOmpContribution } from "./conf
|
|
|
45
47
|
import { buildDshClientConfig, summarizeDsh, buildDshContribution } from "./config-export/dsh";
|
|
46
48
|
import { buildMcodeClientConfig, summarizeMcode, buildMcodeContribution } from "./config-export/mcode";
|
|
47
49
|
import { buildZcodeClientConfig, summarizeZcode, buildZcodeContribution } from "./config-export/zcode";
|
|
50
|
+
import { buildRaycastClientConfig, summarizeRaycast, buildRaycastContribution } from "./config-export/raycast";
|
|
48
51
|
|
|
49
52
|
|
|
50
53
|
|
|
@@ -533,6 +536,22 @@ export function asideConfigPath(env: OpencodeLaunchEnv = process.env, home: stri
|
|
|
533
536
|
return join(asideAccountDir(env, home), "models.json");
|
|
534
537
|
}
|
|
535
538
|
|
|
539
|
+
/**
|
|
540
|
+
* Raycast's Custom Providers directory. Raycast hard-codes
|
|
541
|
+
* `~/.config/raycast/ai` on macOS AND Windows: it neither honors
|
|
542
|
+
* `XDG_CONFIG_HOME` nor ships a variable of its own that relocates the file, so
|
|
543
|
+
* unlike `opencodeGlobalConfigPath` there is no override to mirror and the env
|
|
544
|
+
* parameter exists only to keep the resolver signature uniform with the rest.
|
|
545
|
+
*/
|
|
546
|
+
export function raycastAiDir(_env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
547
|
+
return join(home, ".config", "raycast", "ai");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** The providers file Raycast watches (manual.raycast.com/ai/custom-providers). */
|
|
551
|
+
export function raycastConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
552
|
+
return join(raycastAiDir(env, home), "providers.yaml");
|
|
553
|
+
}
|
|
554
|
+
|
|
536
555
|
/** Endpoint plus admission, identical for the V1 `options` and V2 `settings` field. */
|
|
537
556
|
function opencodeProviderConnection(baseURL: string, config: OcxConfig): OpencodeProviderConnection {
|
|
538
557
|
const options: OpencodeProviderConnection = { baseURL };
|
|
@@ -1259,6 +1278,23 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
|
1259
1278
|
// bind would generate a config that 401s.
|
|
1260
1279
|
loopbackOnly: true,
|
|
1261
1280
|
},
|
|
1281
|
+
raycast: {
|
|
1282
|
+
id: "raycast",
|
|
1283
|
+
// Not a bare `providers.yaml`: same Downloads-folder collision argument as
|
|
1284
|
+
// `aside-models.json`.
|
|
1285
|
+
filename: "raycast-providers.yaml",
|
|
1286
|
+
destination: env => raycastConfigPath(env),
|
|
1287
|
+
apiKeyEnv: "",
|
|
1288
|
+
exportHint: "Raycast reads providers.yaml with no api_keys entry; loopback needs no key.",
|
|
1289
|
+
build: buildRaycastClientConfig,
|
|
1290
|
+
format: "yaml",
|
|
1291
|
+
summarize: summarizeRaycast,
|
|
1292
|
+
buildContribution: buildRaycastContribution,
|
|
1293
|
+
// Raycast's provider entry has no header field, and its `api_keys` value
|
|
1294
|
+
// is read literally (no env interpolation), so the only way to admit a
|
|
1295
|
+
// remote bind would be a plaintext secret on disk. Refuse instead.
|
|
1296
|
+
loopbackOnly: true,
|
|
1297
|
+
},
|
|
1262
1298
|
};
|
|
1263
1299
|
|
|
1264
1300
|
export const EXPORT_CLIENT_IDS: readonly ExportClientId[] = Object.keys(EXPORT_CLIENTS) as ExportClientId[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { CURSOR_CAPABILITIES } from "../adapters/cursor/catalog";
|
|
2
|
+
import { nativeOpenAiCapabilityDisplayName } from "../codex/catalog/metadata";
|
|
3
|
+
import type { ExportModel } from "./config-export/contracts";
|
|
4
|
+
|
|
5
|
+
const KNOWN_ACRONYMS = new Set(["gpt", "glm", "grok"]);
|
|
6
|
+
|
|
7
|
+
function titleWord(word: string): string {
|
|
8
|
+
const lower = word.toLowerCase();
|
|
9
|
+
if (KNOWN_ACRONYMS.has(lower)) return lower.toUpperCase();
|
|
10
|
+
if (/^\d+\.\d+$/.test(word)) return word;
|
|
11
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Last-resort label when no catalog or operator name exists. Joins dotted version
|
|
16
|
+
* tails (`5-1` → `5.1`, `2-5` → `2.5`) so Raycast reads like a product name
|
|
17
|
+
* instead of a slug.
|
|
18
|
+
*/
|
|
19
|
+
function humanizeModelSlug(modelId: string): string {
|
|
20
|
+
const parts = modelId.split("-");
|
|
21
|
+
const words: string[] = [];
|
|
22
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
23
|
+
const part = parts[index]!;
|
|
24
|
+
const next = parts[index + 1];
|
|
25
|
+
if (/^\d+$/.test(part) && next !== undefined && /^\d+$/.test(next)) {
|
|
26
|
+
words.push(`${part}.${next}`);
|
|
27
|
+
index += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
words.push(part);
|
|
31
|
+
}
|
|
32
|
+
return words.map(titleWord).join(" ");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function wireModelId(model: ExportModel): string {
|
|
36
|
+
if (model.id?.trim()) return model.id.trim();
|
|
37
|
+
const slash = model.namespaced.lastIndexOf("/");
|
|
38
|
+
return slash >= 0 ? model.namespaced.slice(slash + 1) : model.namespaced;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Human-facing model label for clients whose picker shows `name` verbatim.
|
|
43
|
+
*
|
|
44
|
+
* Raycast has no second column for provider, so the shared `exportModelLabel`
|
|
45
|
+
* suffix `(anthropic)` would be noise — and its fallback is the raw wire id
|
|
46
|
+
* because management slugs are deliberately withheld from ExportModel. Resolve
|
|
47
|
+
* operator labels first, then the canonical capability tables, then a slug
|
|
48
|
+
* humanizer.
|
|
49
|
+
*/
|
|
50
|
+
export function exportPresentationLabel(model: ExportModel): string {
|
|
51
|
+
const configured = model.displayName?.trim();
|
|
52
|
+
if (configured) return configured;
|
|
53
|
+
const wireId = wireModelId(model);
|
|
54
|
+
const fromCursor = CURSOR_CAPABILITIES[wireId]?.displayName;
|
|
55
|
+
if (fromCursor) return fromCursor;
|
|
56
|
+
if (model.native) {
|
|
57
|
+
const native = nativeOpenAiCapabilityDisplayName(wireId);
|
|
58
|
+
if (native) return native;
|
|
59
|
+
}
|
|
60
|
+
return humanizeModelSlug(wireId);
|
|
61
|
+
}
|
|
@@ -307,6 +307,11 @@ function routedDisplayName(slug: string, model?: CatalogModel, config?: Pick<Ocx
|
|
|
307
307
|
return slug;
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Cria uma entrada nativa ou roteada a partir do snapshot upstream, de um clone
|
|
312
|
+
* do template ou de campos mínimos. Aplica os metadados e limites pertinentes
|
|
313
|
+
* sem alterar o template nem herdar sua marca de nome ou histórico de prioridade.
|
|
314
|
+
*/
|
|
310
315
|
export function deriveEntry(
|
|
311
316
|
template: RawEntry | null,
|
|
312
317
|
slug: string,
|
|
@@ -332,6 +337,7 @@ export function deriveEntry(
|
|
|
332
337
|
}
|
|
333
338
|
if (template || codexForwardNativeCapabilityAlias) {
|
|
334
339
|
const e = JSON.parse(JSON.stringify(codexForwardNativeCapabilityAlias ?? template)) as RawEntry;
|
|
340
|
+
delete e.opencodex_native_display_name;
|
|
335
341
|
// A cached template may carry display-order history; each new row owns its natural rank.
|
|
336
342
|
delete e[SPAWN_PRIORITY_FIELD];
|
|
337
343
|
e.slug = slug;
|
|
@@ -773,6 +779,20 @@ function recoverableNativeSlug(entry: RawEntry): string | null {
|
|
|
773
779
|
: null;
|
|
774
780
|
}
|
|
775
781
|
|
|
782
|
+
/** Undo our display overlay before native metadata normalization and template reuse. */
|
|
783
|
+
function restoreNativeDisplayName(entry: RawEntry): RawEntry {
|
|
784
|
+
const saved = entry.opencodex_native_display_name;
|
|
785
|
+
delete entry.opencodex_native_display_name;
|
|
786
|
+
if (saved && typeof saved === "object" && !Array.isArray(saved)) {
|
|
787
|
+
const label = saved as Record<string, unknown>;
|
|
788
|
+
if (recoverableNativeSlug(entry) === label.slug
|
|
789
|
+
&& typeof label.original === "string" && entry.display_name === label.applied) {
|
|
790
|
+
entry.display_name = label.original;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return entry;
|
|
794
|
+
}
|
|
795
|
+
|
|
776
796
|
/** Append missing supported native rows from trusted catalog sources only. */
|
|
777
797
|
export function mergeCatalogModelsWithNativeRecovery(
|
|
778
798
|
primaryCatalogModels: readonly RawEntry[],
|
|
@@ -862,6 +882,8 @@ export interface ObservedCatalogMergeInput {
|
|
|
862
882
|
readonly suppressedBareNativeSlugs?: ReadonlySet<string>;
|
|
863
883
|
readonly policy: ObservedCatalogMergePolicy;
|
|
864
884
|
readonly openaiContextCap?: NativeContextLimitsInput;
|
|
885
|
+
/** Exact display-only labels for bare native OpenAI models. */
|
|
886
|
+
readonly nativeDisplayNames?: Readonly<Record<string, string>>;
|
|
865
887
|
}
|
|
866
888
|
|
|
867
889
|
/**
|
|
@@ -896,12 +918,14 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
896
918
|
suppressedBareNativeSlugs = new Set(),
|
|
897
919
|
policy,
|
|
898
920
|
openaiContextCap,
|
|
921
|
+
nativeDisplayNames,
|
|
899
922
|
}: ObservedCatalogMergeInput): RawEntry[] {
|
|
900
923
|
// Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at
|
|
901
924
|
// the observed-core boundary so callers can safely retain evidence objects or repeat the merge.
|
|
902
|
-
const detachedCatalogModels = catalogModels
|
|
925
|
+
const detachedCatalogModels = catalogModels
|
|
926
|
+
.map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
|
|
903
927
|
const detachedBaselineCatalogModels = baselineCatalogModels
|
|
904
|
-
.map(entry => structuredClone(entry) as RawEntry);
|
|
928
|
+
.map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
|
|
905
929
|
const detachedRoutedEntries = routedEntries.map(entry => structuredClone(entry) as RawEntry);
|
|
906
930
|
// Track this invocation's generated custom rows, not ownership markers read from disk.
|
|
907
931
|
// Their builder already finalized exact native ladders and ordinary routed mock tiers.
|
|
@@ -1256,6 +1280,17 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
1256
1280
|
);
|
|
1257
1281
|
applyFullModelPickerOrder(versionedEntries, modelPickerOrder);
|
|
1258
1282
|
for (const entry of versionedEntries) {
|
|
1283
|
+
// Templates and account clones must not inherit the native row's overlay marker.
|
|
1284
|
+
delete entry.opencodex_native_display_name;
|
|
1285
|
+
const slug = recoverableNativeSlug(entry);
|
|
1286
|
+
if (slug !== null) {
|
|
1287
|
+
const label = nativeDisplayNames && Object.hasOwn(nativeDisplayNames, slug)
|
|
1288
|
+
? nativeDisplayNames[slug]?.trim() : undefined;
|
|
1289
|
+
if (label && label !== entry.display_name) {
|
|
1290
|
+
entry.opencodex_native_display_name = { slug, original: entry.display_name, applied: label };
|
|
1291
|
+
entry.display_name = label;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1259
1294
|
const kind = entry.opencodex_catalog_kind;
|
|
1260
1295
|
if (trustedAccountBoundNativeCatalogSlug(entry) === undefined
|
|
1261
1296
|
&& kind !== CODEX_CUSTOM_MODEL_CATALOG_KIND
|
|
@@ -1659,6 +1694,12 @@ export function finalizeAutoReviewModelOverride(
|
|
|
1659
1694
|
return applyAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), sourceModels);
|
|
1660
1695
|
}
|
|
1661
1696
|
|
|
1697
|
+
/**
|
|
1698
|
+
* Mescla o catálogo retido com os modelos visíveis e as configurações atuais,
|
|
1699
|
+
* incluindo os nomes nativos. Tenta preservar o backup original e usa a permissão
|
|
1700
|
+
* de escrita para publicar o resultado apenas se os bytes mudarem, retornando
|
|
1701
|
+
* a contagem de entradas roteadas e por conta, o caminho e o estado da gravação.
|
|
1702
|
+
*/
|
|
1662
1703
|
function writeRetainedCatalogSync({
|
|
1663
1704
|
config,
|
|
1664
1705
|
goModels,
|
|
@@ -1880,6 +1921,7 @@ function writeRetainedCatalogSync({
|
|
|
1880
1921
|
accountBoundEntries,
|
|
1881
1922
|
suppressedBareNativeSlugs,
|
|
1882
1923
|
openaiContextCap,
|
|
1924
|
+
nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames,
|
|
1883
1925
|
policy: {
|
|
1884
1926
|
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
1885
1927
|
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
|
package/src/codex/convergence.ts
CHANGED
|
@@ -226,6 +226,12 @@ function bindGatherPaths(
|
|
|
226
226
|
};
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Prepara um candidato de catálogo para convergência sem gravá-lo em disco.
|
|
231
|
+
* Clona a fonte e mescla as observações nativas, os modelos roteados e por conta,
|
|
232
|
+
* aplicando a configuração, inclusive nomes nativos, e os limites de raciocínio
|
|
233
|
+
* observados no runtime antes de retornar o catálogo resultante.
|
|
234
|
+
*/
|
|
229
235
|
function prepareCatalog(
|
|
230
236
|
config: Readonly<OcxConfig>,
|
|
231
237
|
source: Extract<CatalogSourceForGather, { kind: "available" }>,
|
|
@@ -366,6 +372,7 @@ function prepareCatalog(
|
|
|
366
372
|
accountBoundEntries,
|
|
367
373
|
suppressedBareNativeSlugs,
|
|
368
374
|
openaiContextCap,
|
|
375
|
+
nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames,
|
|
369
376
|
policy: {
|
|
370
377
|
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
371
378
|
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
|