@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.
- package/README.md +73 -12
- package/dist/catalog.d.ts +3 -2
- package/dist/cli/index.js +5448 -198
- package/dist/credentials.d.ts +178 -0
- package/dist/direct-launch.d.ts +6 -0
- package/dist/domain.d.ts +118 -8
- package/dist/generated/api.d.ts +150 -5
- package/dist/grok-args.d.ts +1 -0
- package/dist/harness-arguments.d.ts +5 -0
- package/dist/harness-environment.d.ts +1 -0
- package/dist/harness-process.d.ts +11 -0
- package/dist/harness-types.d.ts +1 -0
- package/dist/harnesses.d.ts +33 -2
- package/dist/index.js +163 -2
- package/dist/launcher.d.ts +38 -3
- package/dist/mcp/index.js +123 -3
- package/dist/opencode2-config.d.ts +60 -0
- package/dist/ori-backend.d.ts +89 -0
- package/dist/presets.d.ts +63 -0
- package/dist/runtime.d.ts +20 -0
- package/dist/sdk.d.ts +77 -9
- package/dist/sdk.js +163 -2
- package/dist/serve/index.js +656 -70
- package/dist/server.d.ts +16 -0
- package/dist/service.d.ts +2 -1
- package/dist/terminal-descriptors.d.ts +5 -0
- package/hasna.contract.json +1 -1
- package/openapi.json +320 -6
- package/package.json +9 -4
package/dist/generated/api.d.ts
CHANGED
|
@@ -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 @@
|
|
|
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
|
+
}>;
|
package/dist/harness-types.d.ts
CHANGED
package/dist/harnesses.d.ts
CHANGED
|
@@ -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
|
package/dist/launcher.d.ts
CHANGED
|
@@ -1,9 +1,44 @@
|
|
|
1
1
|
import { SwitcherClient } from "./sdk";
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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>;
|