@hasna/switcher 0.1.2 → 0.1.4
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 +20 -8
- package/dist/cli/index.js +2725 -621
- package/dist/cli.d.ts +2 -0
- package/dist/codex-model-policy.d.ts +65 -0
- package/dist/credentials.d.ts +71 -8
- package/dist/direct-launch.d.ts +2 -2
- package/dist/domain.d.ts +259 -2
- package/dist/gemini-model-policy.d.ts +56 -0
- package/dist/generated/api.d.ts +192 -0
- package/dist/harness-types.d.ts +9 -0
- package/dist/hermes-model-policy.d.ts +30 -0
- package/dist/index.js +138 -48
- package/dist/inference-gateway.d.ts +24 -0
- package/dist/launcher.d.ts +8 -3
- package/dist/mcp/index.js +153 -62
- package/dist/model-policy-schema.d.ts +137 -0
- package/dist/model-policy.d.ts +31 -0
- package/dist/native-model-policy.d.ts +23 -0
- package/dist/opencode-model-policy.d.ts +33 -0
- package/dist/ori-model-policy.d.ts +8 -0
- package/dist/sdk.d.ts +248 -6
- package/dist/sdk.js +138 -48
- package/dist/serve/index.js +1044 -88
- package/docs/MODEL-POLICY.md +82 -0
- package/openapi.json +836 -1
- package/package.json +3 -2
package/dist/generated/api.d.ts
CHANGED
|
@@ -345,6 +345,30 @@ export interface components {
|
|
|
345
345
|
/** @enum {string} */
|
|
346
346
|
harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
|
|
347
347
|
model: string;
|
|
348
|
+
modelPolicy?: {
|
|
349
|
+
/**
|
|
350
|
+
* @default 1
|
|
351
|
+
* @enum {number}
|
|
352
|
+
*/
|
|
353
|
+
version: 1;
|
|
354
|
+
roles?: {
|
|
355
|
+
subagent?: string;
|
|
356
|
+
fast?: string;
|
|
357
|
+
planning?: string;
|
|
358
|
+
review?: string;
|
|
359
|
+
summary?: string;
|
|
360
|
+
compaction?: string;
|
|
361
|
+
weak?: string;
|
|
362
|
+
editor?: string;
|
|
363
|
+
};
|
|
364
|
+
allowedModels?: string[];
|
|
365
|
+
aliases?: {
|
|
366
|
+
[key: string]: string;
|
|
367
|
+
};
|
|
368
|
+
fallbacks?: {
|
|
369
|
+
[key: string]: string[];
|
|
370
|
+
};
|
|
371
|
+
};
|
|
348
372
|
};
|
|
349
373
|
Profile: {
|
|
350
374
|
id: string;
|
|
@@ -353,6 +377,30 @@ export interface components {
|
|
|
353
377
|
/** @enum {string} */
|
|
354
378
|
harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
|
|
355
379
|
model: string;
|
|
380
|
+
modelPolicy?: {
|
|
381
|
+
/**
|
|
382
|
+
* @default 1
|
|
383
|
+
* @enum {number}
|
|
384
|
+
*/
|
|
385
|
+
version: 1;
|
|
386
|
+
roles?: {
|
|
387
|
+
subagent?: string;
|
|
388
|
+
fast?: string;
|
|
389
|
+
planning?: string;
|
|
390
|
+
review?: string;
|
|
391
|
+
summary?: string;
|
|
392
|
+
compaction?: string;
|
|
393
|
+
weak?: string;
|
|
394
|
+
editor?: string;
|
|
395
|
+
};
|
|
396
|
+
allowedModels?: string[];
|
|
397
|
+
aliases?: {
|
|
398
|
+
[key: string]: string;
|
|
399
|
+
};
|
|
400
|
+
fallbacks?: {
|
|
401
|
+
[key: string]: string[];
|
|
402
|
+
};
|
|
403
|
+
};
|
|
356
404
|
version: number;
|
|
357
405
|
updatedAt: string;
|
|
358
406
|
};
|
|
@@ -368,6 +416,44 @@ export interface components {
|
|
|
368
416
|
supportedParameters?: string[];
|
|
369
417
|
supportedGenerationMethods?: string[];
|
|
370
418
|
};
|
|
419
|
+
ModelPolicy: {
|
|
420
|
+
/**
|
|
421
|
+
* @default 1
|
|
422
|
+
* @enum {number}
|
|
423
|
+
*/
|
|
424
|
+
version: 1;
|
|
425
|
+
roles?: {
|
|
426
|
+
subagent?: string;
|
|
427
|
+
fast?: string;
|
|
428
|
+
planning?: string;
|
|
429
|
+
review?: string;
|
|
430
|
+
summary?: string;
|
|
431
|
+
compaction?: string;
|
|
432
|
+
weak?: string;
|
|
433
|
+
editor?: string;
|
|
434
|
+
};
|
|
435
|
+
allowedModels?: string[];
|
|
436
|
+
aliases?: {
|
|
437
|
+
[key: string]: string;
|
|
438
|
+
};
|
|
439
|
+
fallbacks?: {
|
|
440
|
+
[key: string]: string[];
|
|
441
|
+
};
|
|
442
|
+
};
|
|
443
|
+
RoutingEvent: {
|
|
444
|
+
/** Format: date-time */
|
|
445
|
+
at: string;
|
|
446
|
+
requestId: string;
|
|
447
|
+
requestedModel: string;
|
|
448
|
+
resolvedModel?: string;
|
|
449
|
+
reportedModel?: string;
|
|
450
|
+
/** @enum {string} */
|
|
451
|
+
decision: "allow" | "alias" | "reject" | "fallback";
|
|
452
|
+
/** @enum {string} */
|
|
453
|
+
role?: "main" | "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor";
|
|
454
|
+
reason?: string;
|
|
455
|
+
upstreamStatus?: number;
|
|
456
|
+
};
|
|
371
457
|
ModelPage: {
|
|
372
458
|
data: {
|
|
373
459
|
id: string;
|
|
@@ -451,6 +537,30 @@ export interface components {
|
|
|
451
537
|
/** @enum {string} */
|
|
452
538
|
harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
|
|
453
539
|
model: string;
|
|
540
|
+
modelPolicy?: {
|
|
541
|
+
/**
|
|
542
|
+
* @default 1
|
|
543
|
+
* @enum {number}
|
|
544
|
+
*/
|
|
545
|
+
version: 1;
|
|
546
|
+
roles?: {
|
|
547
|
+
subagent?: string;
|
|
548
|
+
fast?: string;
|
|
549
|
+
planning?: string;
|
|
550
|
+
review?: string;
|
|
551
|
+
summary?: string;
|
|
552
|
+
compaction?: string;
|
|
553
|
+
weak?: string;
|
|
554
|
+
editor?: string;
|
|
555
|
+
};
|
|
556
|
+
allowedModels?: string[];
|
|
557
|
+
aliases?: {
|
|
558
|
+
[key: string]: string;
|
|
559
|
+
};
|
|
560
|
+
fallbacks?: {
|
|
561
|
+
[key: string]: string[];
|
|
562
|
+
};
|
|
563
|
+
};
|
|
454
564
|
version: number;
|
|
455
565
|
updatedAt: string;
|
|
456
566
|
};
|
|
@@ -475,22 +585,89 @@ export interface components {
|
|
|
475
585
|
warnings: string[];
|
|
476
586
|
};
|
|
477
587
|
RunInput: {
|
|
588
|
+
/** @enum {number} */
|
|
589
|
+
modelPolicyVersion: 1;
|
|
478
590
|
profileId: string;
|
|
479
591
|
/** @enum {string} */
|
|
480
592
|
harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
|
|
481
593
|
model: string;
|
|
594
|
+
modelPolicy?: {
|
|
595
|
+
/**
|
|
596
|
+
* @default 1
|
|
597
|
+
* @enum {number}
|
|
598
|
+
*/
|
|
599
|
+
version: 1;
|
|
600
|
+
roles?: {
|
|
601
|
+
subagent?: string;
|
|
602
|
+
fast?: string;
|
|
603
|
+
planning?: string;
|
|
604
|
+
review?: string;
|
|
605
|
+
summary?: string;
|
|
606
|
+
compaction?: string;
|
|
607
|
+
weak?: string;
|
|
608
|
+
editor?: string;
|
|
609
|
+
};
|
|
610
|
+
allowedModels?: string[];
|
|
611
|
+
aliases?: {
|
|
612
|
+
[key: string]: string;
|
|
613
|
+
};
|
|
614
|
+
fallbacks?: {
|
|
615
|
+
[key: string]: string[];
|
|
616
|
+
};
|
|
617
|
+
};
|
|
482
618
|
planToken: string;
|
|
483
619
|
};
|
|
484
620
|
RunUpdate: {
|
|
485
621
|
/** @enum {string} */
|
|
486
622
|
status: "exited" | "failed" | "interrupted";
|
|
487
623
|
exitCode: number;
|
|
624
|
+
routingEvents?: {
|
|
625
|
+
/** Format: date-time */
|
|
626
|
+
at: string;
|
|
627
|
+
requestId: string;
|
|
628
|
+
requestedModel: string;
|
|
629
|
+
resolvedModel?: string;
|
|
630
|
+
reportedModel?: string;
|
|
631
|
+
/** @enum {string} */
|
|
632
|
+
decision: "allow" | "alias" | "reject" | "fallback";
|
|
633
|
+
/** @enum {string} */
|
|
634
|
+
role?: "main" | "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor";
|
|
635
|
+
reason?: string;
|
|
636
|
+
upstreamStatus?: number;
|
|
637
|
+
}[];
|
|
638
|
+
routingEventsDropped?: number;
|
|
488
639
|
};
|
|
489
640
|
Run: {
|
|
641
|
+
/** @enum {number} */
|
|
642
|
+
modelPolicyVersion?: 1;
|
|
490
643
|
profileId: string;
|
|
491
644
|
/** @enum {string} */
|
|
492
645
|
harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
|
|
493
646
|
model: string;
|
|
647
|
+
modelPolicy?: {
|
|
648
|
+
/**
|
|
649
|
+
* @default 1
|
|
650
|
+
* @enum {number}
|
|
651
|
+
*/
|
|
652
|
+
version: 1;
|
|
653
|
+
roles?: {
|
|
654
|
+
subagent?: string;
|
|
655
|
+
fast?: string;
|
|
656
|
+
planning?: string;
|
|
657
|
+
review?: string;
|
|
658
|
+
summary?: string;
|
|
659
|
+
compaction?: string;
|
|
660
|
+
weak?: string;
|
|
661
|
+
editor?: string;
|
|
662
|
+
};
|
|
663
|
+
allowedModels?: string[];
|
|
664
|
+
aliases?: {
|
|
665
|
+
[key: string]: string;
|
|
666
|
+
};
|
|
667
|
+
fallbacks?: {
|
|
668
|
+
[key: string]: string[];
|
|
669
|
+
};
|
|
670
|
+
};
|
|
494
671
|
planToken: string;
|
|
495
672
|
version: number;
|
|
496
673
|
updatedAt: string;
|
|
@@ -503,6 +680,21 @@ export interface components {
|
|
|
503
680
|
startedAt: string;
|
|
504
681
|
endedAt?: string;
|
|
505
682
|
exitCode?: number;
|
|
683
|
+
routingEvents?: {
|
|
684
|
+
/** Format: date-time */
|
|
685
|
+
at: string;
|
|
686
|
+
requestId: string;
|
|
687
|
+
requestedModel: string;
|
|
688
|
+
resolvedModel?: string;
|
|
689
|
+
reportedModel?: string;
|
|
690
|
+
/** @enum {string} */
|
|
691
|
+
decision: "allow" | "alias" | "reject" | "fallback";
|
|
692
|
+
/** @enum {string} */
|
|
693
|
+
role?: "main" | "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor";
|
|
694
|
+
reason?: string;
|
|
695
|
+
upstreamStatus?: number;
|
|
696
|
+
}[];
|
|
697
|
+
routingEventsDropped?: number;
|
|
506
698
|
};
|
|
507
699
|
Health: {
|
|
508
700
|
/** @enum {string} */
|
package/dist/harness-types.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Model, Provider, Profile } from "./domain";
|
|
2
|
+
import type { CompiledModelPolicy, ModelPolicy } from "./model-policy";
|
|
3
|
+
import type { NativeModelPolicy } from "./native-model-policy";
|
|
4
|
+
import type { RoutingEvent } from "./inference-gateway";
|
|
2
5
|
export type HarnessId = Profile["harness"];
|
|
3
6
|
export type HarnessLaunchInput = {
|
|
4
7
|
harness: HarnessId;
|
|
@@ -14,6 +17,12 @@ export type HarnessLaunchInput = {
|
|
|
14
17
|
cwd: string;
|
|
15
18
|
version?: string;
|
|
16
19
|
sessionDir?: string;
|
|
20
|
+
providerId?: string;
|
|
21
|
+
providerBaseUrl?: string;
|
|
22
|
+
modelPolicy?: ModelPolicy;
|
|
23
|
+
compiledPolicy?: CompiledModelPolicy;
|
|
24
|
+
nativePolicy?: NativeModelPolicy;
|
|
25
|
+
onRoutingEvent?: (event: RoutingEvent) => void;
|
|
17
26
|
};
|
|
18
27
|
export type PreparedLaunch = {
|
|
19
28
|
executable: string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type HermesModelRoles = {
|
|
2
|
+
subagent?: string;
|
|
3
|
+
fast?: string;
|
|
4
|
+
planning?: string;
|
|
5
|
+
review?: string;
|
|
6
|
+
summary?: string;
|
|
7
|
+
compaction?: string;
|
|
8
|
+
weak?: string;
|
|
9
|
+
editor?: string;
|
|
10
|
+
};
|
|
11
|
+
export type HermesModelPolicyFragment = {
|
|
12
|
+
auxiliary: Record<string, {
|
|
13
|
+
api_mode: string;
|
|
14
|
+
provider: "custom";
|
|
15
|
+
model: string;
|
|
16
|
+
base_url: string;
|
|
17
|
+
api_key_env: "SWITCHER_HERMES_AUX_API_KEY";
|
|
18
|
+
fallback_chain: [];
|
|
19
|
+
}>;
|
|
20
|
+
delegation: {
|
|
21
|
+
model: string;
|
|
22
|
+
provider: "custom";
|
|
23
|
+
base_url: string;
|
|
24
|
+
};
|
|
25
|
+
env: {
|
|
26
|
+
apiKeyEnv: "SWITCHER_HERMES_AUX_API_KEY";
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
/** Compile verified Hermes auxiliary config fields without serializing credentials. */
|
|
30
|
+
export declare function compileHermesModelPolicy(currentModel: string, roles: HermesModelRoles, baseUrl: string, apiMode?: string): HermesModelPolicyFragment;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,56 @@
|
|
|
1
1
|
// src/domain.ts
|
|
2
|
+
import { z as z2 } from "zod";
|
|
3
|
+
|
|
4
|
+
// src/model-policy-schema.ts
|
|
2
5
|
import { z } from "zod";
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var
|
|
6
|
+
var policyModelIdSchema = z.string().min(1).max(300).regex(/^[^\u0000-\u001f\u007f]+$/);
|
|
7
|
+
var modelPolicyRoleSchema = z.enum(["subagent", "fast", "planning", "review", "summary", "compaction", "weak", "editor"]);
|
|
8
|
+
var boundedModelList = z.array(policyModelIdSchema).max(500);
|
|
9
|
+
var aliasSchema = z.string().regex(/^[A-Za-z0-9._/-]{1,120}$/).refine((v) => !["__proto__", "prototype", "constructor"].includes(v));
|
|
10
|
+
var boundedModelMap = z.record(aliasSchema, policyModelIdSchema).superRefine((value, ctx) => {
|
|
11
|
+
if (Object.keys(value).length > 200)
|
|
12
|
+
ctx.addIssue({ code: "custom", message: "Model policy maps may contain at most 200 entries." });
|
|
13
|
+
});
|
|
14
|
+
var fallbacksSchema = z.record(policyModelIdSchema, z.array(policyModelIdSchema).max(20)).superRefine((value, ctx) => {
|
|
15
|
+
if (Object.keys(value).length > 200)
|
|
16
|
+
ctx.addIssue({ code: "custom", message: "Model policy fallback maps may contain at most 200 entries." });
|
|
17
|
+
});
|
|
18
|
+
var modelPolicySchema = z.object({
|
|
19
|
+
version: z.literal(1).default(1),
|
|
20
|
+
roles: z.object({
|
|
21
|
+
subagent: policyModelIdSchema.optional(),
|
|
22
|
+
fast: policyModelIdSchema.optional(),
|
|
23
|
+
planning: policyModelIdSchema.optional(),
|
|
24
|
+
review: policyModelIdSchema.optional(),
|
|
25
|
+
summary: policyModelIdSchema.optional(),
|
|
26
|
+
compaction: policyModelIdSchema.optional(),
|
|
27
|
+
weak: policyModelIdSchema.optional(),
|
|
28
|
+
editor: policyModelIdSchema.optional()
|
|
29
|
+
}).strict().optional(),
|
|
30
|
+
allowedModels: boundedModelList.optional(),
|
|
31
|
+
aliases: boundedModelMap.optional(),
|
|
32
|
+
fallbacks: fallbacksSchema.optional()
|
|
33
|
+
}).strict();
|
|
34
|
+
var routingDecisionSchema = z.enum(["allow", "alias", "reject", "fallback"]);
|
|
35
|
+
var routingEventRoleSchema = z.enum(["main", ...modelPolicyRoleSchema.options]);
|
|
36
|
+
var routingEventSchema = z.object({
|
|
37
|
+
at: z.string().datetime({ offset: true }),
|
|
38
|
+
requestId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/),
|
|
39
|
+
requestedModel: policyModelIdSchema,
|
|
40
|
+
resolvedModel: policyModelIdSchema.optional(),
|
|
41
|
+
reportedModel: policyModelIdSchema.optional(),
|
|
42
|
+
decision: routingDecisionSchema,
|
|
43
|
+
role: routingEventRoleSchema.optional(),
|
|
44
|
+
reason: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/).optional(),
|
|
45
|
+
upstreamStatus: z.number().int().min(100).max(599).optional()
|
|
46
|
+
}).strict();
|
|
47
|
+
var routingEventsSchema = z.array(routingEventSchema).max(1000);
|
|
48
|
+
// src/domain.ts
|
|
49
|
+
var harnessSchema = z2.enum(["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]);
|
|
50
|
+
var protocolSchema = z2.enum(["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]);
|
|
51
|
+
var idSchema = z2.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
|
|
52
|
+
var label = z2.string().min(1).max(200);
|
|
53
|
+
var envRef = z2.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
|
|
8
54
|
function endpoint(value) {
|
|
9
55
|
let url;
|
|
10
56
|
try {
|
|
@@ -17,74 +63,79 @@ function endpoint(value) {
|
|
|
17
63
|
throw new Fault(400, "invalid_url", "URL must use HTTPS, contain no credentials/query/fragment, or use HTTP on loopback.");
|
|
18
64
|
return url.href.replace(/\/+$/, "");
|
|
19
65
|
}
|
|
20
|
-
var urlSchema =
|
|
66
|
+
var urlSchema = z2.string().max(2000).superRefine((v, ctx) => {
|
|
21
67
|
try {
|
|
22
68
|
endpoint(v);
|
|
23
69
|
} catch {
|
|
24
70
|
ctx.addIssue({ code: "custom", message: "Invalid endpoint URL" });
|
|
25
71
|
}
|
|
26
72
|
}).transform(endpoint);
|
|
27
|
-
var modelSchema =
|
|
28
|
-
id:
|
|
73
|
+
var modelSchema = z2.object({
|
|
74
|
+
id: z2.string().min(1).max(300),
|
|
29
75
|
name: label,
|
|
30
|
-
description:
|
|
31
|
-
available:
|
|
32
|
-
contextWindow:
|
|
33
|
-
maxOutputTokens:
|
|
34
|
-
inputModalities:
|
|
35
|
-
outputModalities:
|
|
36
|
-
supportedParameters:
|
|
37
|
-
supportedGenerationMethods:
|
|
76
|
+
description: z2.string().max(8000).optional(),
|
|
77
|
+
available: z2.boolean().optional(),
|
|
78
|
+
contextWindow: z2.number().int().positive().optional(),
|
|
79
|
+
maxOutputTokens: z2.number().int().positive().optional(),
|
|
80
|
+
inputModalities: z2.array(z2.string().max(50)).max(20).optional(),
|
|
81
|
+
outputModalities: z2.array(z2.string().max(50)).max(20).optional(),
|
|
82
|
+
supportedParameters: z2.array(z2.string().max(100)).max(100).optional(),
|
|
83
|
+
supportedGenerationMethods: z2.array(z2.string().min(1).max(100)).max(100).optional()
|
|
38
84
|
}).strict();
|
|
39
|
-
var providerInputSchema =
|
|
85
|
+
var providerInputSchema = z2.object({
|
|
40
86
|
id: idSchema,
|
|
41
87
|
name: label,
|
|
42
88
|
baseUrl: urlSchema,
|
|
43
89
|
protocol: protocolSchema,
|
|
44
90
|
credentialEnv: envRef.optional(),
|
|
45
|
-
authStyle:
|
|
91
|
+
authStyle: z2.enum(["bearer", "x-api-key", "api-key"]).default("bearer"),
|
|
46
92
|
catalogBaseUrl: urlSchema.optional(),
|
|
47
|
-
catalogFormat:
|
|
48
|
-
catalogAuthStyle:
|
|
93
|
+
catalogFormat: z2.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]).optional(),
|
|
94
|
+
catalogAuthStyle: z2.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
|
|
49
95
|
catalogCredentialEnv: envRef.optional(),
|
|
50
|
-
catalogAccountId:
|
|
51
|
-
modelsPath:
|
|
52
|
-
manualModels:
|
|
96
|
+
catalogAccountId: z2.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
|
|
97
|
+
modelsPath: z2.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
|
|
98
|
+
manualModels: z2.array(modelSchema).max(1e4).default([])
|
|
53
99
|
}).strict().refine((p) => !p.modelsPath.split("/").includes("..") && !p.modelsPath.startsWith("/"), "modelsPath must be relative");
|
|
54
|
-
var providerPresetSchema =
|
|
100
|
+
var providerPresetSchema = z2.object({
|
|
55
101
|
id: idSchema,
|
|
56
102
|
name: label,
|
|
57
103
|
credentialEnv: envRef.optional(),
|
|
58
|
-
credentialAliases:
|
|
59
|
-
protocols:
|
|
104
|
+
credentialAliases: z2.array(z2.string().regex(/^[A-Z][A-Z0-9_]+$/)),
|
|
105
|
+
protocols: z2.array(z2.object({
|
|
60
106
|
protocol: protocolSchema,
|
|
61
107
|
baseUrl: urlSchema.optional(),
|
|
62
|
-
authStyle:
|
|
108
|
+
authStyle: z2.enum(["bearer", "x-api-key", "api-key"]),
|
|
63
109
|
catalogBaseUrl: urlSchema.optional(),
|
|
64
|
-
catalogFormat:
|
|
65
|
-
catalogAuthStyle:
|
|
66
|
-
modelsPath:
|
|
67
|
-
notes:
|
|
110
|
+
catalogFormat: z2.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]),
|
|
111
|
+
catalogAuthStyle: z2.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
|
|
112
|
+
modelsPath: z2.string(),
|
|
113
|
+
notes: z2.array(z2.string())
|
|
68
114
|
}).strict()).min(1),
|
|
69
|
-
sources:
|
|
70
|
-
verification:
|
|
115
|
+
sources: z2.array(z2.string().url()),
|
|
116
|
+
verification: z2.literal("documented")
|
|
71
117
|
}).strict();
|
|
72
|
-
var profileInputSchema =
|
|
118
|
+
var profileInputSchema = z2.object({
|
|
73
119
|
id: idSchema,
|
|
74
120
|
name: label,
|
|
75
121
|
providerId: idSchema,
|
|
76
122
|
harness: harnessSchema,
|
|
77
|
-
model:
|
|
123
|
+
model: z2.string().min(1).max(300),
|
|
124
|
+
modelPolicy: modelPolicySchema.optional()
|
|
78
125
|
}).strict();
|
|
79
|
-
var runInputSchema =
|
|
126
|
+
var runInputSchema = z2.object({
|
|
127
|
+
modelPolicyVersion: z2.literal(1),
|
|
80
128
|
profileId: idSchema,
|
|
81
129
|
harness: harnessSchema,
|
|
82
|
-
model:
|
|
83
|
-
|
|
130
|
+
model: z2.string().min(1).max(300),
|
|
131
|
+
modelPolicy: modelPolicySchema.optional(),
|
|
132
|
+
planToken: z2.string().regex(/^[a-f0-9]{64}$/)
|
|
84
133
|
}).strict();
|
|
85
|
-
var runUpdateSchema =
|
|
86
|
-
status:
|
|
87
|
-
exitCode:
|
|
134
|
+
var runUpdateSchema = z2.object({
|
|
135
|
+
status: z2.enum(["exited", "failed", "interrupted"]),
|
|
136
|
+
exitCode: z2.number().int().min(0).max(255),
|
|
137
|
+
routingEvents: routingEventsSchema.optional(),
|
|
138
|
+
routingEventsDropped: z2.number().int().min(0).max(1e6).optional()
|
|
88
139
|
}).strict();
|
|
89
140
|
|
|
90
141
|
class Fault extends Error {
|
|
@@ -149,7 +200,7 @@ async function boundedJson(response, maxBytes = MAX_BYTES) {
|
|
|
149
200
|
}
|
|
150
201
|
|
|
151
202
|
// src/sdk.ts
|
|
152
|
-
import {
|
|
203
|
+
import { createClientTransport } from "@hasna/contracts/client";
|
|
153
204
|
|
|
154
205
|
// src/presets.ts
|
|
155
206
|
var route = (protocol, baseUrl, options = {}) => ({
|
|
@@ -310,6 +361,7 @@ function apiError(status, data, apiKey) {
|
|
|
310
361
|
|
|
311
362
|
class SwitcherClient {
|
|
312
363
|
options;
|
|
364
|
+
transport;
|
|
313
365
|
baseUrl;
|
|
314
366
|
constructor(options) {
|
|
315
367
|
this.baseUrl = endpoint(options.baseUrl).replace(/\/v1$/, "");
|
|
@@ -317,9 +369,50 @@ class SwitcherClient {
|
|
|
317
369
|
throw new Error("Switcher API key is required.");
|
|
318
370
|
this.options = { ...options };
|
|
319
371
|
}
|
|
372
|
+
static fromEnvironment(env = process.env, options = {}) {
|
|
373
|
+
const shared = createClientTransport("switcher", env, {
|
|
374
|
+
credentials: options.credentials,
|
|
375
|
+
timeoutMs: options.timeoutMs ?? 120000,
|
|
376
|
+
retry: false,
|
|
377
|
+
fetchImpl: async (input, init) => {
|
|
378
|
+
const address = String(input).replace(/\/v1\/(health|ready|version)$/, "/$1");
|
|
379
|
+
const key = new Headers(init?.headers).get("x-api-key") ?? "";
|
|
380
|
+
let response;
|
|
381
|
+
try {
|
|
382
|
+
response = await (options.fetch ?? fetch)(address, init);
|
|
383
|
+
} catch {
|
|
384
|
+
throw new SwitcherError(0, "connection_failed", "Switcher API request failed; check endpoint and service availability.");
|
|
385
|
+
}
|
|
386
|
+
if (response.status === 401 || response.status === 403) {
|
|
387
|
+
await response.body?.cancel().catch(() => {});
|
|
388
|
+
throw new SwitcherError(response.status, "api_error", `Switcher API returned HTTP ${response.status}. Check the configured credential; no alternate identity was selected.`);
|
|
389
|
+
}
|
|
390
|
+
let data;
|
|
391
|
+
try {
|
|
392
|
+
data = await boundedJson(response);
|
|
393
|
+
} catch {
|
|
394
|
+
throw new SwitcherError(response.status, "invalid_response", "Switcher API returned invalid JSON.");
|
|
395
|
+
}
|
|
396
|
+
if (!response.ok)
|
|
397
|
+
throw apiError(response.status, data, key);
|
|
398
|
+
return Response.json(data);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
const client = new SwitcherClient({ baseUrl: shared.resolution.baseUrl, apiKey: () => {
|
|
402
|
+
throw new Error("Shared credential transport was not invoked.");
|
|
403
|
+
} });
|
|
404
|
+
client.transport = shared.client;
|
|
405
|
+
return client;
|
|
406
|
+
}
|
|
320
407
|
async request(method, path, body, options = {}) {
|
|
321
408
|
if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) && !["/health", "/ready", "/version"].includes(path) || path.includes(".."))
|
|
322
409
|
throw new Error("Invalid API path.");
|
|
410
|
+
if (this.transport)
|
|
411
|
+
return this.transport.request(method, path.startsWith("/v1/") ? path.slice(3) : path, body, {
|
|
412
|
+
idempotencyKey: method === "GET" ? undefined : options.idempotencyKey ?? crypto.randomUUID(),
|
|
413
|
+
headers: options.version === undefined ? undefined : { "if-match": String(options.version) },
|
|
414
|
+
retry: false
|
|
415
|
+
});
|
|
323
416
|
const apiKey = typeof this.options.apiKey === "function" ? this.options.apiKey() : this.options.apiKey;
|
|
324
417
|
if (!apiKey || /[\r\n]/.test(apiKey))
|
|
325
418
|
throw new Error("Switcher API key is required.");
|
|
@@ -410,17 +503,14 @@ class SwitcherClient {
|
|
|
410
503
|
return this.request("GET", `/v1/runs/${encodeURIComponent(id)}`);
|
|
411
504
|
}
|
|
412
505
|
createRun(input, idempotencyKey) {
|
|
413
|
-
return this.request("POST", "/v1/runs", input, { idempotencyKey });
|
|
506
|
+
return this.request("POST", "/v1/runs", { ...input, modelPolicyVersion: 1 }, { idempotencyKey });
|
|
414
507
|
}
|
|
415
508
|
finishRun(id, version, input, idempotencyKey) {
|
|
416
509
|
return this.request("PATCH", `/v1/runs/${encodeURIComponent(id)}`, input, { version, idempotencyKey });
|
|
417
510
|
}
|
|
418
511
|
}
|
|
419
|
-
function clientFromEnv(env = process.env) {
|
|
420
|
-
|
|
421
|
-
if (!env.HASNA_SWITCHER_API_URL || !credential())
|
|
422
|
-
throw new Error("Set HASNA_SWITCHER_API_URL and HASNA_SWITCHER_API_KEY; no local database fallback is available.");
|
|
423
|
-
return new SwitcherClient({ baseUrl: env.HASNA_SWITCHER_API_URL, apiKey: credential });
|
|
512
|
+
function clientFromEnv(env = process.env, options = {}) {
|
|
513
|
+
return SwitcherClient.fromEnvironment(env, options);
|
|
424
514
|
}
|
|
425
515
|
export {
|
|
426
516
|
providerFromPreset,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HarnessLaunchInput } from "./harness-types";
|
|
2
|
+
import { type CompiledModelPolicy } from "./model-policy";
|
|
3
|
+
export type RoutingEvent = {
|
|
4
|
+
at: string;
|
|
5
|
+
requestId: string;
|
|
6
|
+
requestedModel: string;
|
|
7
|
+
resolvedModel?: string;
|
|
8
|
+
reportedModel?: string;
|
|
9
|
+
decision: "allow" | "alias" | "reject" | "fallback";
|
|
10
|
+
reason?: string;
|
|
11
|
+
upstreamStatus?: number;
|
|
12
|
+
};
|
|
13
|
+
type GatewayInput = HarnessLaunchInput & {
|
|
14
|
+
compiledPolicy: CompiledModelPolicy;
|
|
15
|
+
catalogPath: string;
|
|
16
|
+
onRoutingEvent?: (event: RoutingEvent) => void;
|
|
17
|
+
};
|
|
18
|
+
/** A per-launch credential boundary: the native client never receives the provider key. */
|
|
19
|
+
export declare function createInferenceGateway(input: GatewayInput): {
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
token: string;
|
|
22
|
+
cleanup: () => Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
export {};
|
package/dist/launcher.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { PreparedLaunch } from "./harness-types";
|
|
1
2
|
import { SwitcherClient } from "./sdk";
|
|
2
3
|
import { type LaunchPlan, type ProviderInput } from "./domain";
|
|
3
|
-
import { type OriContract
|
|
4
|
+
import { type OriContract } from "./ori-backend";
|
|
5
|
+
import type { RoutingEvent } from "./inference-gateway";
|
|
4
6
|
export { childEnvironment } from "./harness-environment";
|
|
5
7
|
export type LaunchBackend = "direct" | "ori";
|
|
6
8
|
export type LaunchOptions = {
|
|
@@ -18,6 +20,7 @@ export type LaunchOptions = {
|
|
|
18
20
|
type OriPreparationOptions = Pick<LaunchOptions, "oriExecutable" | "args" | "resolveCredential" | "credentialEnv"> & {
|
|
19
21
|
stateDir?: string;
|
|
20
22
|
cwd?: string;
|
|
23
|
+
onRoutingEvent?: (event: RoutingEvent) => void;
|
|
21
24
|
};
|
|
22
25
|
type OriSupportedHarness = Exclude<LaunchPlan["profile"]["harness"], "omp" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "opencode" | "kilo">;
|
|
23
26
|
export declare function validateOriForPlan(plan: LaunchPlan, options?: Pick<OriPreparationOptions, "oriExecutable" | "args" | "credentialEnv" | "cwd">): Promise<{
|
|
@@ -38,8 +41,10 @@ declare function buildOriRequest(plan: LaunchPlan, target: OriSupportedHarness,
|
|
|
38
41
|
};
|
|
39
42
|
readonly args: string[];
|
|
40
43
|
};
|
|
41
|
-
export declare function prepareOriForPlan(plan: LaunchPlan, options
|
|
44
|
+
export declare function prepareOriForPlan(plan: LaunchPlan, options: OriPreparationOptions & {
|
|
45
|
+
stateDir: string;
|
|
46
|
+
}): Promise<{
|
|
42
47
|
contract: OriContract;
|
|
43
|
-
prepared:
|
|
48
|
+
prepared: PreparedLaunch;
|
|
44
49
|
}>;
|
|
45
50
|
export declare function launch(client: SwitcherClient, profileId: string, options?: LaunchOptions): Promise<number>;
|