@hasna/switcher 0.1.0 → 0.1.1

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.
@@ -63,6 +63,38 @@ export interface paths {
63
63
  patch?: never;
64
64
  trace?: never;
65
65
  };
66
+ "/v1/provider-presets": {
67
+ parameters: {
68
+ query?: never;
69
+ header?: never;
70
+ path?: never;
71
+ cookie?: never;
72
+ };
73
+ get: operations["listProviderPresets"];
74
+ put?: never;
75
+ post?: never;
76
+ delete?: never;
77
+ options?: never;
78
+ head?: never;
79
+ patch?: never;
80
+ trace?: never;
81
+ };
82
+ "/v1/provider-presets/{id}": {
83
+ parameters: {
84
+ query?: never;
85
+ header?: never;
86
+ path?: never;
87
+ cookie?: never;
88
+ };
89
+ get: operations["getProviderPreset"];
90
+ put?: never;
91
+ post?: never;
92
+ delete?: never;
93
+ options?: never;
94
+ head?: never;
95
+ patch?: never;
96
+ trace?: never;
97
+ };
66
98
  "/v1/providers/{id}/models": {
67
99
  parameters: {
68
100
  query?: never;
@@ -211,6 +243,29 @@ export interface paths {
211
243
  export type webhooks = Record<string, never>;
212
244
  export interface components {
213
245
  schemas: {
246
+ ProviderPreset: {
247
+ id: string;
248
+ name: string;
249
+ credentialEnv?: string;
250
+ credentialAliases: string[];
251
+ protocols: {
252
+ /** @enum {string} */
253
+ protocol: "anthropic-messages" | "openai-responses" | "openai-chat";
254
+ baseUrl?: string;
255
+ /** @enum {string} */
256
+ authStyle: "bearer" | "x-api-key";
257
+ catalogBaseUrl?: string;
258
+ /** @enum {string} */
259
+ catalogFormat: "openai" | "ollama" | "mistral" | "together" | "fireworks" | "dashscope" | "none";
260
+ /** @enum {string} */
261
+ catalogAuthStyle?: "bearer" | "x-api-key" | "none";
262
+ modelsPath: string;
263
+ notes: string[];
264
+ }[];
265
+ sources: string[];
266
+ /** @enum {string} */
267
+ verification: "documented";
268
+ };
214
269
  ProviderInput: {
215
270
  id: string;
216
271
  name: string;
@@ -223,6 +278,13 @@ export interface components {
223
278
  * @enum {string}
224
279
  */
225
280
  authStyle: "bearer" | "x-api-key";
281
+ catalogBaseUrl?: string;
282
+ /** @enum {string} */
283
+ catalogFormat?: "openai" | "ollama" | "mistral" | "together" | "fireworks" | "dashscope" | "none";
284
+ /** @enum {string} */
285
+ catalogAuthStyle?: "bearer" | "x-api-key" | "none";
286
+ catalogCredentialEnv?: string;
287
+ catalogAccountId?: string;
226
288
  /** @default models */
227
289
  modelsPath: string;
228
290
  /** @default [] */
@@ -230,6 +292,7 @@ export interface components {
230
292
  id: string;
231
293
  name: string;
232
294
  description?: string;
295
+ available?: boolean;
233
296
  contextWindow?: number;
234
297
  maxOutputTokens?: number;
235
298
  inputModalities?: string[];
@@ -249,6 +312,13 @@ export interface components {
249
312
  * @enum {string}
250
313
  */
251
314
  authStyle: "bearer" | "x-api-key";
315
+ catalogBaseUrl?: string;
316
+ /** @enum {string} */
317
+ catalogFormat?: "openai" | "ollama" | "mistral" | "together" | "fireworks" | "dashscope" | "none";
318
+ /** @enum {string} */
319
+ catalogAuthStyle?: "bearer" | "x-api-key" | "none";
320
+ catalogCredentialEnv?: string;
321
+ catalogAccountId?: string;
252
322
  /** @default models */
253
323
  modelsPath: string;
254
324
  /** @default [] */
@@ -256,6 +326,7 @@ export interface components {
256
326
  id: string;
257
327
  name: string;
258
328
  description?: string;
329
+ available?: boolean;
259
330
  contextWindow?: number;
260
331
  maxOutputTokens?: number;
261
332
  inputModalities?: string[];
@@ -270,7 +341,7 @@ export interface components {
270
341
  name: string;
271
342
  providerId: string;
272
343
  /** @enum {string} */
273
- harness: "claude" | "codex" | "grok" | "opencode2";
344
+ harness: "claude" | "codex" | "grok" | "opencode2" | "pi";
274
345
  model: string;
275
346
  };
276
347
  Profile: {
@@ -278,7 +349,7 @@ export interface components {
278
349
  name: string;
279
350
  providerId: string;
280
351
  /** @enum {string} */
281
- harness: "claude" | "codex" | "grok" | "opencode2";
352
+ harness: "claude" | "codex" | "grok" | "opencode2" | "pi";
282
353
  model: string;
283
354
  version: number;
284
355
  updatedAt: string;
@@ -287,6 +358,7 @@ export interface components {
287
358
  id: string;
288
359
  name: string;
289
360
  description?: string;
361
+ available?: boolean;
290
362
  contextWindow?: number;
291
363
  maxOutputTokens?: number;
292
364
  inputModalities?: string[];
@@ -298,6 +370,7 @@ export interface components {
298
370
  id: string;
299
371
  name: string;
300
372
  description?: string;
373
+ available?: boolean;
301
374
  contextWindow?: number;
302
375
  maxOutputTokens?: number;
303
376
  inputModalities?: string[];
@@ -317,6 +390,7 @@ export interface components {
317
390
  id: string;
318
391
  name: string;
319
392
  description?: string;
393
+ available?: boolean;
320
394
  contextWindow?: number;
321
395
  maxOutputTokens?: number;
322
396
  inputModalities?: string[];
@@ -340,6 +414,13 @@ export interface components {
340
414
  * @enum {string}
341
415
  */
342
416
  authStyle: "bearer" | "x-api-key";
417
+ catalogBaseUrl?: string;
418
+ /** @enum {string} */
419
+ catalogFormat?: "openai" | "ollama" | "mistral" | "together" | "fireworks" | "dashscope" | "none";
420
+ /** @enum {string} */
421
+ catalogAuthStyle?: "bearer" | "x-api-key" | "none";
422
+ catalogCredentialEnv?: string;
423
+ catalogAccountId?: string;
343
424
  /** @default models */
344
425
  modelsPath: string;
345
426
  /** @default [] */
@@ -347,6 +428,7 @@ export interface components {
347
428
  id: string;
348
429
  name: string;
349
430
  description?: string;
431
+ available?: boolean;
350
432
  contextWindow?: number;
351
433
  maxOutputTokens?: number;
352
434
  inputModalities?: string[];
@@ -361,7 +443,7 @@ export interface components {
361
443
  name: string;
362
444
  providerId: string;
363
445
  /** @enum {string} */
364
- harness: "claude" | "codex" | "grok" | "opencode2";
446
+ harness: "claude" | "codex" | "grok" | "opencode2" | "pi";
365
447
  model: string;
366
448
  version: number;
367
449
  updatedAt: string;
@@ -371,6 +453,7 @@ export interface components {
371
453
  id: string;
372
454
  name: string;
373
455
  description?: string;
456
+ available?: boolean;
374
457
  contextWindow?: number;
375
458
  maxOutputTokens?: number;
376
459
  inputModalities?: string[];
@@ -387,7 +470,7 @@ export interface components {
387
470
  RunInput: {
388
471
  profileId: string;
389
472
  /** @enum {string} */
390
- harness: "claude" | "codex" | "grok" | "opencode2";
473
+ harness: "claude" | "codex" | "grok" | "opencode2" | "pi";
391
474
  model: string;
392
475
  planToken: string;
393
476
  };
@@ -399,7 +482,7 @@ export interface components {
399
482
  Run: {
400
483
  profileId: string;
401
484
  /** @enum {string} */
402
- harness: "claude" | "codex" | "grok" | "opencode2";
485
+ harness: "claude" | "codex" | "grok" | "opencode2" | "pi";
403
486
  model: string;
404
487
  planToken: string;
405
488
  version: number;
@@ -804,6 +887,68 @@ export interface operations {
804
887
  };
805
888
  };
806
889
  };
890
+ listProviderPresets: {
891
+ parameters: {
892
+ query?: never;
893
+ header?: never;
894
+ path?: never;
895
+ cookie?: never;
896
+ };
897
+ requestBody?: never;
898
+ responses: {
899
+ /** @description Success */
900
+ 200: {
901
+ headers: {
902
+ [name: string]: unknown;
903
+ };
904
+ content: {
905
+ "application/json": {
906
+ data: components["schemas"]["ProviderPreset"][];
907
+ };
908
+ };
909
+ };
910
+ /** @description Structured error */
911
+ default: {
912
+ headers: {
913
+ [name: string]: unknown;
914
+ };
915
+ content: {
916
+ "application/json": components["schemas"]["Error"];
917
+ };
918
+ };
919
+ };
920
+ };
921
+ getProviderPreset: {
922
+ parameters: {
923
+ query?: never;
924
+ header?: never;
925
+ path: {
926
+ id: string;
927
+ };
928
+ cookie?: never;
929
+ };
930
+ requestBody?: never;
931
+ responses: {
932
+ /** @description Success */
933
+ 200: {
934
+ headers: {
935
+ [name: string]: unknown;
936
+ };
937
+ content: {
938
+ "application/json": components["schemas"]["ProviderPreset"];
939
+ };
940
+ };
941
+ /** @description Structured error */
942
+ default: {
943
+ headers: {
944
+ [name: string]: unknown;
945
+ };
946
+ content: {
947
+ "application/json": components["schemas"]["Error"];
948
+ };
949
+ };
950
+ };
951
+ };
807
952
  listModels: {
808
953
  parameters: {
809
954
  query?: {
@@ -0,0 +1 @@
1
+ export declare function validateGrokResume(args: string[]): void;
@@ -0,0 +1,5 @@
1
+ import type { HarnessId } from "./harness-types";
2
+ export declare function assertHarnessArguments(harness: HarnessId, args: readonly string[], options?: {
3
+ additionalReserved?: readonly string[];
4
+ reserveCodexConfig?: boolean;
5
+ }): void;
@@ -0,0 +1 @@
1
+ export declare function childEnvironment(env?: NodeJS.ProcessEnv): Record<string, string>;
@@ -0,0 +1,11 @@
1
+ /** Own a POSIX process group; interactive children get a controlling terminal. */
2
+ export declare function runHarnessProcess(options: {
3
+ executable: string;
4
+ args: string[];
5
+ cwd: string;
6
+ env: NodeJS.ProcessEnv;
7
+ timeoutMs?: number;
8
+ }): Promise<{
9
+ code: number;
10
+ interrupted: boolean;
11
+ }>;
@@ -13,6 +13,7 @@ export type HarnessLaunchInput = {
13
13
  stateDir: string;
14
14
  cwd: string;
15
15
  version?: string;
16
+ sessionDir?: string;
16
17
  };
17
18
  export type PreparedLaunch = {
18
19
  executable: string;
@@ -1,13 +1,44 @@
1
1
  import type { HarnessId, HarnessLaunchInput, PreparedLaunch } from "./harness-types";
2
2
  export declare function detectHarness(harness: HarnessId, override?: string): Promise<{
3
- harness: "claude" | "codex" | "grok" | "opencode2";
3
+ harness: "claude" | "codex" | "grok" | "opencode2" | "pi";
4
4
  executable: string;
5
5
  available: boolean;
6
6
  version: string;
7
7
  } | {
8
- harness: "claude" | "codex" | "grok" | "opencode2";
8
+ harness: "claude" | "codex" | "grok" | "opencode2" | "pi";
9
9
  executable: string;
10
10
  available: boolean;
11
11
  version: undefined;
12
12
  }>;
13
+ export declare function validateHarnessVersion(harness: HarnessId, version: string | undefined): void;
14
+ export declare function codexModel(model: HarnessLaunchInput["models"][number], priority: number): {
15
+ slug: string;
16
+ display_name: string;
17
+ description: string;
18
+ shell_type: string;
19
+ visibility: string;
20
+ supported_in_api: boolean;
21
+ priority: number;
22
+ supported_reasoning_levels: never[];
23
+ default_reasoning_level: null;
24
+ support_verbosity: boolean;
25
+ supports_reasoning_summary_parameter: boolean;
26
+ default_verbosity: null;
27
+ supports_parallel_tool_calls: boolean;
28
+ apply_patch_tool_type: null;
29
+ truncation_policy: {
30
+ mode: string;
31
+ limit: number;
32
+ };
33
+ experimental_supported_tools: never[];
34
+ context_window: number | null;
35
+ max_context_window: number | null;
36
+ input_modalities: ("text" | "image")[];
37
+ prefer_websockets: boolean;
38
+ use_responses_lite: boolean;
39
+ model_messages: {
40
+ instructions_template: string;
41
+ instructions_variables: null;
42
+ };
43
+ };
13
44
  export declare function prepareHarnessLaunch(input: HarnessLaunchInput): Promise<PreparedLaunch>;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/domain.ts
2
2
  import { z } from "zod";
3
- var harnessSchema = z.enum(["claude", "codex", "grok", "opencode2"]);
3
+ var harnessSchema = z.enum(["claude", "codex", "grok", "opencode2", "pi"]);
4
4
  var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat"]);
5
5
  var idSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
6
6
  var label = z.string().min(1).max(200);
@@ -28,6 +28,7 @@ var modelSchema = z.object({
28
28
  id: z.string().min(1).max(300),
29
29
  name: label,
30
30
  description: z.string().max(8000).optional(),
31
+ available: z.boolean().optional(),
31
32
  contextWindow: z.number().int().positive().optional(),
32
33
  maxOutputTokens: z.number().int().positive().optional(),
33
34
  inputModalities: z.array(z.string().max(50)).max(20).optional(),
@@ -41,9 +42,32 @@ var providerInputSchema = z.object({
41
42
  protocol: protocolSchema,
42
43
  credentialEnv: envRef.optional(),
43
44
  authStyle: z.enum(["bearer", "x-api-key"]).default("bearer"),
45
+ catalogBaseUrl: urlSchema.optional(),
46
+ catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "none"]).optional(),
47
+ catalogAuthStyle: z.enum(["bearer", "x-api-key", "none"]).optional(),
48
+ catalogCredentialEnv: envRef.optional(),
49
+ catalogAccountId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
44
50
  modelsPath: z.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
45
51
  manualModels: z.array(modelSchema).max(1e4).default([])
46
52
  }).strict().refine((p) => !p.modelsPath.split("/").includes("..") && !p.modelsPath.startsWith("/"), "modelsPath must be relative");
53
+ var providerPresetSchema = z.object({
54
+ id: idSchema,
55
+ name: label,
56
+ credentialEnv: envRef.optional(),
57
+ credentialAliases: z.array(z.string().regex(/^[A-Z][A-Z0-9_]+$/)),
58
+ protocols: z.array(z.object({
59
+ protocol: protocolSchema,
60
+ baseUrl: urlSchema.optional(),
61
+ authStyle: z.enum(["bearer", "x-api-key"]),
62
+ catalogBaseUrl: urlSchema.optional(),
63
+ catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "none"]),
64
+ catalogAuthStyle: z.enum(["bearer", "x-api-key", "none"]).optional(),
65
+ modelsPath: z.string(),
66
+ notes: z.array(z.string())
67
+ }).strict()).min(1),
68
+ sources: z.array(z.string().url()),
69
+ verification: z.literal("documented")
70
+ }).strict();
47
71
  var profileInputSchema = z.object({
48
72
  id: idSchema,
49
73
  name: label,
@@ -71,6 +95,15 @@ class Fault extends Error {
71
95
  this.code = code;
72
96
  }
73
97
  }
98
+ function parse(schema, value) {
99
+ const result = schema.safeParse(value);
100
+ if (!result.success)
101
+ throw new Fault(400, "invalid_request", result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; "));
102
+ return result.data;
103
+ }
104
+ function compatible(harness, protocol) {
105
+ return harness === "claude" ? protocol === "anthropic-messages" : harness === "codex" ? protocol === "openai-responses" : true;
106
+ }
74
107
 
75
108
  // src/http.ts
76
109
  var MAX_BYTES = 16 * 1024 * 1024;
@@ -109,6 +142,118 @@ async function boundedJson(response, maxBytes = MAX_BYTES) {
109
142
  // src/sdk.ts
110
143
  import { resolveCredential } from "@hasna/contracts/client";
111
144
 
145
+ // src/presets.ts
146
+ var route = (protocol, baseUrl, options = {}) => ({
147
+ protocol,
148
+ baseUrl,
149
+ authStyle: "bearer",
150
+ catalogFormat: "openai",
151
+ modelsPath: "models",
152
+ notes: [],
153
+ ...options
154
+ });
155
+ var preset = (id, name, protocols, sources, alias) => parse(providerPresetSchema, {
156
+ id,
157
+ name,
158
+ protocols,
159
+ sources,
160
+ credentialAliases: alias ? [alias] : [],
161
+ credentialEnv: alias ? `SWITCHER_PROVIDER_${id.toUpperCase().replace(/-/g, "_")}` : undefined,
162
+ verification: "documented"
163
+ });
164
+ var providerPresets = [
165
+ preset("deepseek", "DeepSeek", [
166
+ route("openai-chat", "https://api.deepseek.com", { catalogBaseUrl: "https://api.deepseek.com" }),
167
+ route("anthropic-messages", "https://api.deepseek.com/anthropic/v1", { catalogBaseUrl: "https://api.deepseek.com" })
168
+ ], ["https://api-docs.deepseek.com/guides/anthropic_api", "https://api-docs.deepseek.com/api/list-models"], "DEEPSEEK_API_KEY"),
169
+ preset("openrouter", "OpenRouter", ["openai-chat", "openai-responses", "anthropic-messages"].map((protocol) => route(protocol, "https://openrouter.ai/api/v1", { catalogAuthStyle: "none" })), ["https://openrouter.ai/docs/api/api-reference/models/list-all-models-and-their-properties", "https://openrouter.ai/docs/guides/overview"], "OPENROUTER_API_KEY"),
170
+ preset("anthropic", "Anthropic", [route("anthropic-messages", "https://api.anthropic.com/v1", { authStyle: "x-api-key" })], ["https://platform.claude.com/docs/en/api/overview", "https://platform.claude.com/docs/en/api/models/list"], "ANTHROPIC_API_KEY"),
171
+ preset("openai", "OpenAI", [route("openai-responses", "https://api.openai.com/v1"), route("openai-chat", "https://api.openai.com/v1")], ["https://platform.openai.com/docs/api-reference/introduction", "https://platform.openai.com/docs/api-reference/models/list"], "OPENAI_API_KEY"),
172
+ preset("xai", "xAI", ["openai-chat", "openai-responses", "anthropic-messages"].map((protocol) => route(protocol, "https://api.x.ai/v1")), ["https://api.x.ai/docs/", "https://docs.x.ai/developers/model-capabilities/text/generate-text"], "XAI_API_KEY"),
173
+ preset("ollama", "Ollama", ["openai-chat", "openai-responses"].map((protocol) => route(protocol, "http://127.0.0.1:11434/v1", {
174
+ catalogBaseUrl: "http://127.0.0.1:11434",
175
+ modelsPath: "api/tags",
176
+ catalogFormat: "ollama",
177
+ catalogAuthStyle: "none",
178
+ notes: protocol === "openai-responses" ? ["Requires Ollama 0.13.3 or newer; only stateless Responses are supported."] : []
179
+ })), ["https://docs.ollama.com/api/openai-compatibility", "https://docs.ollama.com/api/tags"]),
180
+ preset("lmstudio", "LM Studio", ["openai-chat", "openai-responses", "anthropic-messages"].map((protocol) => route(protocol, "http://127.0.0.1:1234/v1", {
181
+ notes: ["Server authentication is optional. Use --credential-env when authentication is enabled."]
182
+ })), ["https://lmstudio.ai/docs/developer/rest"]),
183
+ preset("vllm", "vLLM (operator endpoint)", [
184
+ route("openai-chat", undefined, { notes: ["Pass the operator's OpenAI-compatible URL, normally ending in /v1. vLLM exposes Chat Completions at /chat/completions and GET /models under that prefix; configure --credential-env only when the server was started with --api-key or VLLM_API_KEY."] }),
185
+ route("openai-responses", undefined, { notes: ["Pass the operator's OpenAI-compatible URL, normally ending in /v1. Responses is available for supported text-generation models at /responses; configure --credential-env only when the server was started with --api-key or VLLM_API_KEY."] }),
186
+ route("anthropic-messages", undefined, { notes: ["Pass the operator's URL, normally ending in /v1. vLLM exposes the Anthropic Messages API at /messages for supported deployments. Chat templates and the configured tool parser determine whether streaming and tool calls work for a served model; configure --credential-env only when the server was started with --api-key or VLLM_API_KEY."] })
187
+ ], ["https://docs.vllm.ai/en/latest/serving/online_serving/openai_compatible_server/", "https://github.com/vllm-project/vllm/blob/main/docs/serving/online_serving/README.md"]),
188
+ preset("litellm", "LiteLLM Proxy (operator endpoint)", [
189
+ route("openai-chat", undefined, { notes: ["Pass the proxy's documented OpenAI-compatible base URL exactly; the official quick start uses the root server URL, while a deployment may add a prefix such as /v1. LiteLLM serves Chat Completions and GET /models relative to that URL; use --credential-env for the proxy's configured master key or other bearer token."] }),
190
+ route("openai-responses", undefined, { notes: ["Pass the proxy's documented OpenAI-compatible base URL exactly; LiteLLM documents the Responses API under the same proxy root or deployment prefix. Use --credential-env for the proxy's configured master key or other bearer token."] }),
191
+ route("anthropic-messages", undefined, { notes: ["Pass the complete inference prefix ending in /v1, including any deployment prefix. LiteLLM registers /v1/messages; Switcher appends /messages to the stored prefix and discovers /models there. This is a gateway adapter: streaming and tool behavior depend on the configured upstream model and route, so verify those capabilities independently. Use --credential-env for the proxy's configured master key or other bearer token."] })
192
+ ], ["https://docs.litellm.ai/", "https://docs.litellm.ai/docs/proxy/quick_start", "https://github.com/BerriAI/litellm/blob/main/litellm/proxy/anthropic_endpoints/endpoints.py"]),
193
+ preset("groq", "Groq", [route("openai-chat", "https://api.groq.com/openai/v1"), route("openai-responses", "https://api.groq.com/openai/v1", { notes: ["Responses is an upstream beta API."] })], ["https://console.groq.com/docs/api-reference"], "GROQ_API_KEY"),
194
+ preset("cerebras", "Cerebras", [route("openai-chat", "https://api.cerebras.ai/v1")], ["https://inference-docs.cerebras.ai/api-reference/chat-completions"], "CEREBRAS_API_KEY"),
195
+ preset("mistral", "Mistral", [route("openai-chat", "https://api.mistral.ai/v1", { catalogFormat: "mistral" })], ["https://docs.mistral.ai/api/endpoint/chat", "https://docs.mistral.ai/api/endpoint/models"], "MISTRAL_API_KEY"),
196
+ preset("together", "Together AI", [route("openai-chat", "https://api.together.ai/v1", { catalogFormat: "together" })], ["https://docs.together.ai/docs/inference/openai-compatibility", "https://docs.together.ai/reference/models"], "TOGETHER_API_KEY"),
197
+ preset("fireworks", "Fireworks AI", [
198
+ route("openai-chat", "https://api.fireworks.ai/inference/v1", { catalogFormat: "fireworks", notes: ["Model discovery uses GET /v1/accounts/{account_id}/models; provide --catalog-account-id or --catalog-url."] }),
199
+ route("openai-responses", "https://api.fireworks.ai/inference/v1", { catalogFormat: "fireworks", notes: ["Model discovery uses GET /v1/accounts/{account_id}/models; provide --catalog-account-id or --catalog-url."] }),
200
+ route("anthropic-messages", "https://api.fireworks.ai/inference/v1", { catalogFormat: "fireworks", notes: ["Model discovery uses GET /v1/accounts/{account_id}/models; provide --catalog-account-id or --catalog-url."] })
201
+ ], ["https://docs.fireworks.ai/getting-started/quickstart", "https://docs.fireworks.ai/tools-sdks/python-client/api-reference", "https://docs.fireworks.ai/api-reference/anthropic-messages", "https://docs.fireworks.ai/api-reference/post-chatcompletions", "https://docs.fireworks.ai/api-reference/list-models"], "FIREWORKS_API_KEY"),
202
+ preset("moonshot", "Moonshot AI (Kimi)", [route("openai-chat", "https://api.moonshot.ai/v1", { catalogBaseUrl: "https://api.moonshot.ai/v1" })], ["https://platform.kimi.ai/docs/api/chat", "https://platform.kimi.ai/docs/api/list-models"], "MOONSHOT_API_KEY"),
203
+ preset("dashscope", "Alibaba Cloud Model Studio (Qwen)", [route("openai-chat", "https://dashscope-us.aliyuncs.com/compatible-mode/v1", {
204
+ catalogFormat: "none",
205
+ notes: ["Inference keys and endpoints are region/workspace-specific. Model discovery uses GET /api/v1/models on a documented region or workspace catalog URL; pass --catalog-url and --catalog-format dashscope."]
206
+ })], ["https://help.aliyun.com/en/model-studio/base-url", "https://help.aliyun.com/en/model-studio/compatibility-of-openai-with-dashscope", "https://help.aliyun.com/en/model-studio/list-models"], "DASHSCOPE_API_KEY"),
207
+ preset("zai", "Z.AI", [route("openai-chat", "https://api.z.ai/api/paas/v4", {
208
+ catalogFormat: "none",
209
+ notes: ["The published API reference documents inference endpoints but no model-list endpoint; use manual models or provide an explicit catalog URL and parser."]
210
+ })], ["https://docs.z.ai/api-reference/introduction", "https://docs.z.ai/devpack/quick-start"], "ZAI_API_KEY"),
211
+ preset("minimax", "MiniMax", [
212
+ route("openai-chat", "https://api.minimax.cn/v1", { catalogBaseUrl: "https://api.minimax.cn/v1", notes: ["The Open Platform contract uses api.minimax.cn and Bearer auth. Token Plan documentation uses api.minimaxi.com; select that authority explicitly with --url and matching auth/credential settings."] }),
213
+ route("anthropic-messages", "https://api.minimax.cn/anthropic/v1", { authStyle: "x-api-key", catalogBaseUrl: "https://api.minimax.cn/anthropic/v1", catalogAuthStyle: "x-api-key", notes: ["The Open Platform contract uses api.minimax.cn/anthropic/v1 and X-Api-Key. Token Plan documentation uses api.minimaxi.com/anthropic; select that authority explicitly with --url and matching auth/credential settings."] })
214
+ ], ["https://platform.minimaxi.com/docs/api-reference/text-chat-anthropic", "https://platform.minimaxi.com/docs/api-reference/models/anthropic/list-models", "https://platform.minimaxi.com/docs/api-reference/models/openai/list-models", "https://platform.minimaxi.com/docs/token-plan/other-tools"], "MINIMAX_API_KEY"),
215
+ preset("siliconflow", "SiliconFlow", [route("openai-chat", "https://api.siliconflow.cn/v1", { catalogBaseUrl: "https://api.siliconflow.cn/v1", catalogFormat: "openai", notes: ["The official SiliconCloud OpenAPI contract defines GET /models with Bearer auth and data[] model rows; optional type and sub_type filters are available at the upstream endpoint."] })], ["https://github.com/siliconflow/siliconcloud/blob/main/openapi.yaml", "https://docs.siliconflow.cn/docs/userguide/quickstart", "https://docs.siliconflow.cn/docs/api/chat-completions-post"], "SILICONFLOW_API_KEY"),
216
+ ...["anthropic-messages", "openai-responses", "openai-chat"].map((protocol) => preset(`generic-${protocol}`, `Custom ${protocol}`, [route(protocol)], []))
217
+ ];
218
+ function getProviderPreset(id) {
219
+ const entry = providerPresets.find((p) => p.id === id);
220
+ if (!entry)
221
+ throw new Fault(404, "preset_not_found", "Unknown provider preset. Use switcher providers presets to list available presets.");
222
+ return structuredClone(entry);
223
+ }
224
+ function providerFromPreset(presetId, options = {}) {
225
+ const preset2 = getProviderPreset(presetId);
226
+ const selected = preset2.protocols.find((p) => (!options.protocol || p.protocol === options.protocol) && (!options.harness || compatible(options.harness, p.protocol)));
227
+ if (!selected)
228
+ throw new Fault(422, "protocol_mismatch", "This provider preset has no native protocol compatible with the requested harness. Choose an explicitly compatible gateway.");
229
+ const baseUrl = options.baseUrl ?? selected.baseUrl;
230
+ if (!baseUrl)
231
+ throw new Fault(400, "endpoint_required", "This preset requires an explicit --url for its inference endpoint.");
232
+ if (options.baseUrl && selected.baseUrl && new URL(endpoint(options.baseUrl)).origin !== new URL(selected.baseUrl).origin && preset2.credentialEnv && !options.credentialEnv)
233
+ throw new Fault(422, "credential_authority", "An endpoint on another origin requires an explicit --credential-env reference.");
234
+ const suffix = selected.protocol === "anthropic-messages" ? "messages" : selected.protocol === "openai-responses" ? "responses" : "chat";
235
+ if (presetId === "fireworks" && !options.catalogBaseUrl && !options.catalogAccountId)
236
+ throw new Fault(400, "catalog_account_required", "Fireworks model discovery requires --catalog-account-id or an explicit --catalog-url.");
237
+ if (selected.catalogFormat === "none" && options.catalogFormat && !options.catalogBaseUrl)
238
+ throw new Fault(400, "catalog_url_required", "This preset requires an explicit --catalog-url when enabling a catalog parser.");
239
+ const catalogBaseUrl = options.catalogBaseUrl ?? (presetId === "fireworks" && options.catalogAccountId ? `https://api.fireworks.ai/v1/accounts/${encodeURIComponent(options.catalogAccountId)}` : options.baseUrl ? undefined : selected.catalogBaseUrl);
240
+ return parse(providerInputSchema, {
241
+ id: options.id ?? `${preset2.id}-${suffix}`,
242
+ name: preset2.name,
243
+ baseUrl,
244
+ protocol: selected.protocol,
245
+ credentialEnv: options.credentialEnv ?? preset2.credentialEnv,
246
+ authStyle: options.authStyle ?? selected.authStyle,
247
+ catalogBaseUrl,
248
+ catalogCredentialEnv: options.catalogCredentialEnv,
249
+ catalogAuthStyle: options.catalogAuthStyle ?? selected.catalogAuthStyle,
250
+ catalogFormat: options.catalogFormat ?? selected.catalogFormat,
251
+ catalogAccountId: options.catalogAccountId,
252
+ modelsPath: options.modelsPath ?? selected.modelsPath
253
+ });
254
+ }
255
+
256
+ // src/sdk.ts
112
257
  class SwitcherError extends Error {
113
258
  status;
114
259
  code;
@@ -131,7 +276,7 @@ class SwitcherClient {
131
276
  this.options = { ...options };
132
277
  }
133
278
  async request(method, path, body, options = {}) {
134
- if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) || path.includes(".."))
279
+ if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) && !["/health", "/ready", "/version"].includes(path) || path.includes(".."))
135
280
  throw new Error("Invalid API path.");
136
281
  const apiKey = typeof this.options.apiKey === "function" ? this.options.apiKey() : this.options.apiKey;
137
282
  if (!apiKey || /[\r\n]/.test(apiKey))
@@ -162,6 +307,21 @@ class SwitcherClient {
162
307
  query(options = {}) {
163
308
  return new URLSearchParams(Object.entries(options).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)])).toString();
164
309
  }
310
+ health() {
311
+ return this.request("GET", "/health");
312
+ }
313
+ ready() {
314
+ return this.request("GET", "/ready");
315
+ }
316
+ version() {
317
+ return this.request("GET", "/version");
318
+ }
319
+ listProviderPresets() {
320
+ return this.request("GET", "/v1/provider-presets");
321
+ }
322
+ getProviderPreset(id) {
323
+ return this.request("GET", `/v1/provider-presets/${encodeURIComponent(id)}`);
324
+ }
165
325
  listProviders(options = {}) {
166
326
  return this.request("GET", `/v1/providers?${this.query(options)}`);
167
327
  }
@@ -221,6 +381,7 @@ function clientFromEnv(env = process.env) {
221
381
  return new SwitcherClient({ baseUrl: env.HASNA_SWITCHER_API_URL, apiKey: credential });
222
382
  }
223
383
  export {
384
+ providerFromPreset,
224
385
  clientFromEnv,
225
386
  SwitcherError,
226
387
  SwitcherClient
@@ -1,9 +1,44 @@
1
1
  import { SwitcherClient } from "./sdk";
2
- export declare function childEnvironment(env?: NodeJS.ProcessEnv): Record<string, string>;
3
- export declare function launch(client: SwitcherClient, profileId: string, options?: {
2
+ import { type LaunchPlan, type ProviderInput } from "./domain";
3
+ import { type OriContract, type OriLaunchPlan } from "./ori-backend";
4
+ export { childEnvironment } from "./harness-environment";
5
+ export type LaunchBackend = "direct" | "ori";
6
+ export type LaunchOptions = {
7
+ backend?: LaunchBackend;
8
+ oriExecutable?: string;
4
9
  cwd?: string;
5
10
  executable?: string;
6
11
  stateDir?: string;
7
12
  args?: string[];
8
13
  timeoutMs?: number;
9
- }): Promise<number>;
14
+ refresh?: boolean;
15
+ credentialEnv?: NodeJS.ProcessEnv;
16
+ resolveCredential?: (provider: ProviderInput) => Promise<string | undefined>;
17
+ };
18
+ type OriPreparationOptions = Pick<LaunchOptions, "oriExecutable" | "args" | "resolveCredential" | "credentialEnv"> & {
19
+ stateDir?: string;
20
+ cwd?: string;
21
+ };
22
+ export declare function validateOriForPlan(plan: LaunchPlan, options?: Pick<OriPreparationOptions, "oriExecutable" | "args" | "credentialEnv" | "cwd">): Promise<{
23
+ contract: OriContract;
24
+ request: ReturnType<typeof buildOriRequest>;
25
+ warnings: string[];
26
+ }>;
27
+ declare function buildOriRequest(plan: LaunchPlan, catalogPath: string | undefined, args: string[]): {
28
+ readonly target: "claude" | "codex" | "grok" | "opencode2" | "pi";
29
+ readonly provider: string;
30
+ readonly providerBaseUrl: string;
31
+ readonly protocol: "anthropic-messages" | "openai-responses" | "openai-chat";
32
+ readonly model: string;
33
+ readonly catalog: {
34
+ readonly codexModelCatalogPath?: string | undefined;
35
+ readonly source: "switcher-openrouter";
36
+ readonly modelIds: string[];
37
+ };
38
+ readonly args: string[];
39
+ };
40
+ export declare function prepareOriForPlan(plan: LaunchPlan, options?: OriPreparationOptions): Promise<{
41
+ contract: OriContract;
42
+ prepared: OriLaunchPlan;
43
+ }>;
44
+ export declare function launch(client: SwitcherClient, profileId: string, options?: LaunchOptions): Promise<number>;