@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/dist/mcp/index.js CHANGED
@@ -4,16 +4,63 @@
4
4
  // src/mcp.ts
5
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
- import { z as z2 } from "zod";
7
+ import { z as z3 } from "zod";
8
8
 
9
9
  // src/domain.ts
10
+ import { z as z2 } from "zod";
11
+
12
+ // src/model-policy-schema.ts
10
13
  import { z } from "zod";
11
- var VERSION = "0.1.2";
12
- var harnessSchema = z.enum(["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]);
13
- var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]);
14
- var idSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
15
- var label = z.string().min(1).max(200);
16
- var envRef = z.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
14
+ var policyModelIdSchema = z.string().min(1).max(300).regex(/^[^\u0000-\u001f\u007f]+$/);
15
+ var modelPolicyRoleSchema = z.enum(["subagent", "fast", "planning", "review", "summary", "compaction", "weak", "editor"]);
16
+ var boundedModelList = z.array(policyModelIdSchema).max(500);
17
+ var aliasSchema = z.string().regex(/^[A-Za-z0-9._/-]{1,120}$/).refine((v) => !["__proto__", "prototype", "constructor"].includes(v));
18
+ var boundedModelMap = z.record(aliasSchema, policyModelIdSchema).superRefine((value, ctx) => {
19
+ if (Object.keys(value).length > 200)
20
+ ctx.addIssue({ code: "custom", message: "Model policy maps may contain at most 200 entries." });
21
+ });
22
+ var fallbacksSchema = z.record(policyModelIdSchema, z.array(policyModelIdSchema).max(20)).superRefine((value, ctx) => {
23
+ if (Object.keys(value).length > 200)
24
+ ctx.addIssue({ code: "custom", message: "Model policy fallback maps may contain at most 200 entries." });
25
+ });
26
+ var modelPolicySchema = z.object({
27
+ version: z.literal(1).default(1),
28
+ roles: z.object({
29
+ subagent: policyModelIdSchema.optional(),
30
+ fast: policyModelIdSchema.optional(),
31
+ planning: policyModelIdSchema.optional(),
32
+ review: policyModelIdSchema.optional(),
33
+ summary: policyModelIdSchema.optional(),
34
+ compaction: policyModelIdSchema.optional(),
35
+ weak: policyModelIdSchema.optional(),
36
+ editor: policyModelIdSchema.optional()
37
+ }).strict().optional(),
38
+ allowedModels: boundedModelList.optional(),
39
+ aliases: boundedModelMap.optional(),
40
+ fallbacks: fallbacksSchema.optional()
41
+ }).strict();
42
+ var routingDecisionSchema = z.enum(["allow", "alias", "reject", "fallback"]);
43
+ var routingEventRoleSchema = z.enum(["main", ...modelPolicyRoleSchema.options]);
44
+ var routingEventSchema = z.object({
45
+ at: z.string().datetime({ offset: true }),
46
+ requestId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/),
47
+ requestedModel: policyModelIdSchema,
48
+ resolvedModel: policyModelIdSchema.optional(),
49
+ reportedModel: policyModelIdSchema.optional(),
50
+ decision: routingDecisionSchema,
51
+ role: routingEventRoleSchema.optional(),
52
+ reason: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/).optional(),
53
+ upstreamStatus: z.number().int().min(100).max(599).optional()
54
+ }).strict();
55
+ var routingEventsSchema = z.array(routingEventSchema).max(1000);
56
+
57
+ // src/domain.ts
58
+ var VERSION = "0.1.4";
59
+ var harnessSchema = z2.enum(["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]);
60
+ var protocolSchema = z2.enum(["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]);
61
+ var idSchema = z2.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
62
+ var label = z2.string().min(1).max(200);
63
+ var envRef = z2.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
17
64
  function endpoint(value) {
18
65
  let url;
19
66
  try {
@@ -26,74 +73,79 @@ function endpoint(value) {
26
73
  throw new Fault(400, "invalid_url", "URL must use HTTPS, contain no credentials/query/fragment, or use HTTP on loopback.");
27
74
  return url.href.replace(/\/+$/, "");
28
75
  }
29
- var urlSchema = z.string().max(2000).superRefine((v, ctx) => {
76
+ var urlSchema = z2.string().max(2000).superRefine((v, ctx) => {
30
77
  try {
31
78
  endpoint(v);
32
79
  } catch {
33
80
  ctx.addIssue({ code: "custom", message: "Invalid endpoint URL" });
34
81
  }
35
82
  }).transform(endpoint);
36
- var modelSchema = z.object({
37
- id: z.string().min(1).max(300),
83
+ var modelSchema = z2.object({
84
+ id: z2.string().min(1).max(300),
38
85
  name: label,
39
- description: z.string().max(8000).optional(),
40
- available: z.boolean().optional(),
41
- contextWindow: z.number().int().positive().optional(),
42
- maxOutputTokens: z.number().int().positive().optional(),
43
- inputModalities: z.array(z.string().max(50)).max(20).optional(),
44
- outputModalities: z.array(z.string().max(50)).max(20).optional(),
45
- supportedParameters: z.array(z.string().max(100)).max(100).optional(),
46
- supportedGenerationMethods: z.array(z.string().min(1).max(100)).max(100).optional()
86
+ description: z2.string().max(8000).optional(),
87
+ available: z2.boolean().optional(),
88
+ contextWindow: z2.number().int().positive().optional(),
89
+ maxOutputTokens: z2.number().int().positive().optional(),
90
+ inputModalities: z2.array(z2.string().max(50)).max(20).optional(),
91
+ outputModalities: z2.array(z2.string().max(50)).max(20).optional(),
92
+ supportedParameters: z2.array(z2.string().max(100)).max(100).optional(),
93
+ supportedGenerationMethods: z2.array(z2.string().min(1).max(100)).max(100).optional()
47
94
  }).strict();
48
- var providerInputSchema = z.object({
95
+ var providerInputSchema = z2.object({
49
96
  id: idSchema,
50
97
  name: label,
51
98
  baseUrl: urlSchema,
52
99
  protocol: protocolSchema,
53
100
  credentialEnv: envRef.optional(),
54
- authStyle: z.enum(["bearer", "x-api-key", "api-key"]).default("bearer"),
101
+ authStyle: z2.enum(["bearer", "x-api-key", "api-key"]).default("bearer"),
55
102
  catalogBaseUrl: urlSchema.optional(),
56
- catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]).optional(),
57
- catalogAuthStyle: z.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
103
+ catalogFormat: z2.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]).optional(),
104
+ catalogAuthStyle: z2.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
58
105
  catalogCredentialEnv: envRef.optional(),
59
- catalogAccountId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
60
- modelsPath: z.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
61
- manualModels: z.array(modelSchema).max(1e4).default([])
106
+ catalogAccountId: z2.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
107
+ modelsPath: z2.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
108
+ manualModels: z2.array(modelSchema).max(1e4).default([])
62
109
  }).strict().refine((p) => !p.modelsPath.split("/").includes("..") && !p.modelsPath.startsWith("/"), "modelsPath must be relative");
63
- var providerPresetSchema = z.object({
110
+ var providerPresetSchema = z2.object({
64
111
  id: idSchema,
65
112
  name: label,
66
113
  credentialEnv: envRef.optional(),
67
- credentialAliases: z.array(z.string().regex(/^[A-Z][A-Z0-9_]+$/)),
68
- protocols: z.array(z.object({
114
+ credentialAliases: z2.array(z2.string().regex(/^[A-Z][A-Z0-9_]+$/)),
115
+ protocols: z2.array(z2.object({
69
116
  protocol: protocolSchema,
70
117
  baseUrl: urlSchema.optional(),
71
- authStyle: z.enum(["bearer", "x-api-key", "api-key"]),
118
+ authStyle: z2.enum(["bearer", "x-api-key", "api-key"]),
72
119
  catalogBaseUrl: urlSchema.optional(),
73
- catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]),
74
- catalogAuthStyle: z.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
75
- modelsPath: z.string(),
76
- notes: z.array(z.string())
120
+ catalogFormat: z2.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]),
121
+ catalogAuthStyle: z2.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
122
+ modelsPath: z2.string(),
123
+ notes: z2.array(z2.string())
77
124
  }).strict()).min(1),
78
- sources: z.array(z.string().url()),
79
- verification: z.literal("documented")
125
+ sources: z2.array(z2.string().url()),
126
+ verification: z2.literal("documented")
80
127
  }).strict();
81
- var profileInputSchema = z.object({
128
+ var profileInputSchema = z2.object({
82
129
  id: idSchema,
83
130
  name: label,
84
131
  providerId: idSchema,
85
132
  harness: harnessSchema,
86
- model: z.string().min(1).max(300)
133
+ model: z2.string().min(1).max(300),
134
+ modelPolicy: modelPolicySchema.optional()
87
135
  }).strict();
88
- var runInputSchema = z.object({
136
+ var runInputSchema = z2.object({
137
+ modelPolicyVersion: z2.literal(1),
89
138
  profileId: idSchema,
90
139
  harness: harnessSchema,
91
- model: z.string().min(1).max(300),
92
- planToken: z.string().regex(/^[a-f0-9]{64}$/)
140
+ model: z2.string().min(1).max(300),
141
+ modelPolicy: modelPolicySchema.optional(),
142
+ planToken: z2.string().regex(/^[a-f0-9]{64}$/)
93
143
  }).strict();
94
- var runUpdateSchema = z.object({
95
- status: z.enum(["exited", "failed", "interrupted"]),
96
- exitCode: z.number().int().min(0).max(255)
144
+ var runUpdateSchema = z2.object({
145
+ status: z2.enum(["exited", "failed", "interrupted"]),
146
+ exitCode: z2.number().int().min(0).max(255),
147
+ routingEvents: routingEventsSchema.optional(),
148
+ routingEventsDropped: z2.number().int().min(0).max(1e6).optional()
97
149
  }).strict();
98
150
 
99
151
  class Fault extends Error {
@@ -147,7 +199,7 @@ async function boundedJson(response, maxBytes = MAX_BYTES) {
147
199
  }
148
200
 
149
201
  // src/sdk.ts
150
- import { resolveCredential } from "@hasna/contracts/client";
202
+ import { createClientTransport } from "@hasna/contracts/client";
151
203
 
152
204
  // src/presets.ts
153
205
  var route = (protocol, baseUrl, options = {}) => ({
@@ -269,6 +321,7 @@ function apiError(status, data, apiKey) {
269
321
 
270
322
  class SwitcherClient {
271
323
  options;
324
+ transport;
272
325
  baseUrl;
273
326
  constructor(options) {
274
327
  this.baseUrl = endpoint(options.baseUrl).replace(/\/v1$/, "");
@@ -276,9 +329,50 @@ class SwitcherClient {
276
329
  throw new Error("Switcher API key is required.");
277
330
  this.options = { ...options };
278
331
  }
332
+ static fromEnvironment(env = process.env, options = {}) {
333
+ const shared = createClientTransport("switcher", env, {
334
+ credentials: options.credentials,
335
+ timeoutMs: options.timeoutMs ?? 120000,
336
+ retry: false,
337
+ fetchImpl: async (input, init) => {
338
+ const address = String(input).replace(/\/v1\/(health|ready|version)$/, "/$1");
339
+ const key = new Headers(init?.headers).get("x-api-key") ?? "";
340
+ let response;
341
+ try {
342
+ response = await (options.fetch ?? fetch)(address, init);
343
+ } catch {
344
+ throw new SwitcherError(0, "connection_failed", "Switcher API request failed; check endpoint and service availability.");
345
+ }
346
+ if (response.status === 401 || response.status === 403) {
347
+ await response.body?.cancel().catch(() => {});
348
+ throw new SwitcherError(response.status, "api_error", `Switcher API returned HTTP ${response.status}. Check the configured credential; no alternate identity was selected.`);
349
+ }
350
+ let data;
351
+ try {
352
+ data = await boundedJson(response);
353
+ } catch {
354
+ throw new SwitcherError(response.status, "invalid_response", "Switcher API returned invalid JSON.");
355
+ }
356
+ if (!response.ok)
357
+ throw apiError(response.status, data, key);
358
+ return Response.json(data);
359
+ }
360
+ });
361
+ const client = new SwitcherClient({ baseUrl: shared.resolution.baseUrl, apiKey: () => {
362
+ throw new Error("Shared credential transport was not invoked.");
363
+ } });
364
+ client.transport = shared.client;
365
+ return client;
366
+ }
279
367
  async request(method, path, body, options = {}) {
280
368
  if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) && !["/health", "/ready", "/version"].includes(path) || path.includes(".."))
281
369
  throw new Error("Invalid API path.");
370
+ if (this.transport)
371
+ return this.transport.request(method, path.startsWith("/v1/") ? path.slice(3) : path, body, {
372
+ idempotencyKey: method === "GET" ? undefined : options.idempotencyKey ?? crypto.randomUUID(),
373
+ headers: options.version === undefined ? undefined : { "if-match": String(options.version) },
374
+ retry: false
375
+ });
282
376
  const apiKey = typeof this.options.apiKey === "function" ? this.options.apiKey() : this.options.apiKey;
283
377
  if (!apiKey || /[\r\n]/.test(apiKey))
284
378
  throw new Error("Switcher API key is required.");
@@ -369,22 +463,19 @@ class SwitcherClient {
369
463
  return this.request("GET", `/v1/runs/${encodeURIComponent(id)}`);
370
464
  }
371
465
  createRun(input, idempotencyKey) {
372
- return this.request("POST", "/v1/runs", input, { idempotencyKey });
466
+ return this.request("POST", "/v1/runs", { ...input, modelPolicyVersion: 1 }, { idempotencyKey });
373
467
  }
374
468
  finishRun(id, version, input, idempotencyKey) {
375
469
  return this.request("PATCH", `/v1/runs/${encodeURIComponent(id)}`, input, { version, idempotencyKey });
376
470
  }
377
471
  }
378
- function clientFromEnv(env = process.env) {
379
- const credential = () => resolveCredential("switcher", Object.fromEntries(Object.entries(env).filter(([name]) => name === "HASNA_SWITCHER_API_KEY")), { keychain: { enabled: false } })?.apiKey ?? "";
380
- if (!env.HASNA_SWITCHER_API_URL || !credential())
381
- throw new Error("Set HASNA_SWITCHER_API_URL and HASNA_SWITCHER_API_KEY; no local database fallback is available.");
382
- return new SwitcherClient({ baseUrl: env.HASNA_SWITCHER_API_URL, apiKey: credential });
472
+ function clientFromEnv(env = process.env, options = {}) {
473
+ return SwitcherClient.fromEnvironment(env, options);
383
474
  }
384
475
 
385
476
  // src/mcp.ts
386
477
  var server = new McpServer({ name: "switcher", version: VERSION });
387
- var page = { limit: z2.number().int().min(1).max(1000).optional(), offset: z2.number().int().nonnegative().optional(), search: z2.string().optional() };
478
+ var page = { limit: z3.number().int().min(1).max(1000).optional(), offset: z3.number().int().nonnegative().optional(), search: z3.string().optional() };
388
479
  function tool(name, description, schema, run) {
389
480
  server.tool(name, description, schema, async (input) => {
390
481
  try {
@@ -395,26 +486,26 @@ function tool(name, description, schema, run) {
395
486
  });
396
487
  }
397
488
  tool("providers_list", "List provider profiles.", page, (p) => clientFromEnv().listProviders(p));
398
- tool("providers_get", "Get a provider.", { id: z2.string() }, (p) => clientFromEnv().getProvider(p.id));
489
+ tool("providers_get", "Get a provider.", { id: z3.string() }, (p) => clientFromEnv().getProvider(p.id));
399
490
  tool("providers_create", "Create a provider using credential environment references only.", providerInputSchema.innerType().shape, (p) => clientFromEnv().createProvider(p));
400
- tool("providers_update", "Replace a provider at its current version.", { provider: providerInputSchema, version: z2.number().int() }, (p) => clientFromEnv().updateProvider(p.provider, p.version));
401
- tool("providers_delete", "Delete an unreferenced provider.", { id: z2.string(), version: z2.number().int() }, (p) => clientFromEnv().deleteProvider(p.id, p.version));
402
- tool("models_list", "List catalog with capability information.", { id: z2.string(), ...page }, (p) => {
491
+ tool("providers_update", "Replace a provider at its current version.", { provider: providerInputSchema, version: z3.number().int() }, (p) => clientFromEnv().updateProvider(p.provider, p.version));
492
+ tool("providers_delete", "Delete an unreferenced provider.", { id: z3.string(), version: z3.number().int() }, (p) => clientFromEnv().deleteProvider(p.id, p.version));
493
+ tool("models_list", "List catalog with capability information.", { id: z3.string(), ...page }, (p) => {
403
494
  const { id, ...rest } = p;
404
495
  return clientFromEnv().listModels(id, rest);
405
496
  });
406
- tool("models_refresh", "Discover provider models.", { id: z2.string() }, (p) => clientFromEnv().refreshModels(p.id));
497
+ tool("models_refresh", "Discover provider models.", { id: z3.string() }, (p) => clientFromEnv().refreshModels(p.id));
407
498
  tool("profiles_list", "List harness launch profiles.", page, (p) => clientFromEnv().listProfiles(p));
408
- tool("profiles_get", "Get a harness profile.", { id: z2.string() }, (p) => clientFromEnv().getProfile(p.id));
499
+ tool("profiles_get", "Get a harness profile.", { id: z3.string() }, (p) => clientFromEnv().getProfile(p.id));
409
500
  tool("profiles_create", "Create a harness launch profile.", profileInputSchema.shape, (p) => clientFromEnv().createProfile(p));
410
- tool("profiles_update", "Replace a harness profile at its version.", { profile: profileInputSchema, version: z2.number().int() }, (p) => clientFromEnv().updateProfile(p.profile, p.version));
411
- tool("profiles_delete", "Delete a profile without run history.", { id: z2.string(), version: z2.number().int() }, (p) => clientFromEnv().deleteProfile(p.id, p.version));
412
- tool("launch_plan", "Validate a local launch plan; does not execute a remote process.", { profileId: z2.string() }, (p) => clientFromEnv().launchPlan(p.profileId));
501
+ tool("profiles_update", "Replace a harness profile at its version.", { profile: profileInputSchema, version: z3.number().int() }, (p) => clientFromEnv().updateProfile(p.profile, p.version));
502
+ tool("profiles_delete", "Delete a profile without run history.", { id: z3.string(), version: z3.number().int() }, (p) => clientFromEnv().deleteProfile(p.id, p.version));
503
+ tool("launch_plan", "Validate a local launch plan; does not execute a remote process.", { profileId: z3.string() }, (p) => clientFromEnv().launchPlan(p.profileId));
413
504
  tool("runs_list", "List launch metadata.", page, (p) => clientFromEnv().listRuns(p));
414
- tool("runs_get", "Get launch metadata.", { id: z2.string() }, (p) => clientFromEnv().getRun(p.id));
505
+ tool("runs_get", "Get launch metadata.", { id: z3.string() }, (p) => clientFromEnv().getRun(p.id));
415
506
  if (process.argv.includes("--version"))
416
507
  console.log(VERSION);
417
508
  else if (process.argv.includes("--help"))
418
- console.log("switcher-mcp: authenticated Switcher API tools over MCP stdio. Requires HASNA_SWITCHER_API_URL and HASNA_SWITCHER_API_KEY.");
509
+ console.log("switcher-mcp: authenticated Switcher API tools over MCP stdio. Resolves API URL/key through @hasna/contracts (Keychain, canonical config/credentials, or environment).");
419
510
  else
420
511
  await server.connect(new StdioServerTransport);
@@ -0,0 +1,137 @@
1
+ import { z } from "zod";
2
+ /** Model references are opaque provider IDs, never prompts or credentials. */
3
+ export declare const policyModelIdSchema: z.ZodString;
4
+ export declare const modelPolicyRoleSchema: z.ZodEnum<["subagent", "fast", "planning", "review", "summary", "compaction", "weak", "editor"]>;
5
+ export type ModelPolicyRole = z.infer<typeof modelPolicyRoleSchema>;
6
+ export declare const modelPolicySchema: z.ZodObject<{
7
+ version: z.ZodDefault<z.ZodLiteral<1>>;
8
+ roles: z.ZodOptional<z.ZodObject<{
9
+ subagent: z.ZodOptional<z.ZodString>;
10
+ fast: z.ZodOptional<z.ZodString>;
11
+ planning: z.ZodOptional<z.ZodString>;
12
+ review: z.ZodOptional<z.ZodString>;
13
+ summary: z.ZodOptional<z.ZodString>;
14
+ compaction: z.ZodOptional<z.ZodString>;
15
+ weak: z.ZodOptional<z.ZodString>;
16
+ editor: z.ZodOptional<z.ZodString>;
17
+ }, "strict", z.ZodTypeAny, {
18
+ subagent?: string | undefined;
19
+ fast?: string | undefined;
20
+ planning?: string | undefined;
21
+ review?: string | undefined;
22
+ summary?: string | undefined;
23
+ compaction?: string | undefined;
24
+ weak?: string | undefined;
25
+ editor?: string | undefined;
26
+ }, {
27
+ subagent?: string | undefined;
28
+ fast?: string | undefined;
29
+ planning?: string | undefined;
30
+ review?: string | undefined;
31
+ summary?: string | undefined;
32
+ compaction?: string | undefined;
33
+ weak?: string | undefined;
34
+ editor?: string | undefined;
35
+ }>>;
36
+ allowedModels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
37
+ aliases: z.ZodOptional<z.ZodEffects<z.ZodRecord<z.ZodEffects<z.ZodString, string, string>, z.ZodString>, Record<string, string>, Record<string, string>>>;
38
+ fallbacks: z.ZodOptional<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString, "many">>, Record<string, string[]>, Record<string, string[]>>>;
39
+ }, "strict", z.ZodTypeAny, {
40
+ version: 1;
41
+ roles?: {
42
+ subagent?: string | undefined;
43
+ fast?: string | undefined;
44
+ planning?: string | undefined;
45
+ review?: string | undefined;
46
+ summary?: string | undefined;
47
+ compaction?: string | undefined;
48
+ weak?: string | undefined;
49
+ editor?: string | undefined;
50
+ } | undefined;
51
+ allowedModels?: string[] | undefined;
52
+ aliases?: Record<string, string> | undefined;
53
+ fallbacks?: Record<string, string[]> | undefined;
54
+ }, {
55
+ version?: 1 | undefined;
56
+ roles?: {
57
+ subagent?: string | undefined;
58
+ fast?: string | undefined;
59
+ planning?: string | undefined;
60
+ review?: string | undefined;
61
+ summary?: string | undefined;
62
+ compaction?: string | undefined;
63
+ weak?: string | undefined;
64
+ editor?: string | undefined;
65
+ } | undefined;
66
+ allowedModels?: string[] | undefined;
67
+ aliases?: Record<string, string> | undefined;
68
+ fallbacks?: Record<string, string[]> | undefined;
69
+ }>;
70
+ export type ModelPolicy = z.infer<typeof modelPolicySchema>;
71
+ export declare const routingDecisionSchema: z.ZodEnum<["allow", "alias", "reject", "fallback"]>;
72
+ export declare const routingEventRoleSchema: z.ZodEnum<["main", "subagent", "fast", "planning", "review", "summary", "compaction", "weak", "editor"]>;
73
+ export declare const routingEventSchema: z.ZodObject<{
74
+ at: z.ZodString;
75
+ requestId: z.ZodString;
76
+ requestedModel: z.ZodString;
77
+ resolvedModel: z.ZodOptional<z.ZodString>;
78
+ reportedModel: z.ZodOptional<z.ZodString>;
79
+ decision: z.ZodEnum<["allow", "alias", "reject", "fallback"]>;
80
+ role: z.ZodOptional<z.ZodEnum<["main", "subagent", "fast", "planning", "review", "summary", "compaction", "weak", "editor"]>>;
81
+ reason: z.ZodOptional<z.ZodString>;
82
+ upstreamStatus: z.ZodOptional<z.ZodNumber>;
83
+ }, "strict", z.ZodTypeAny, {
84
+ at: string;
85
+ requestId: string;
86
+ requestedModel: string;
87
+ decision: "allow" | "alias" | "reject" | "fallback";
88
+ resolvedModel?: string | undefined;
89
+ reportedModel?: string | undefined;
90
+ role?: "main" | "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor" | undefined;
91
+ reason?: string | undefined;
92
+ upstreamStatus?: number | undefined;
93
+ }, {
94
+ at: string;
95
+ requestId: string;
96
+ requestedModel: string;
97
+ decision: "allow" | "alias" | "reject" | "fallback";
98
+ resolvedModel?: string | undefined;
99
+ reportedModel?: string | undefined;
100
+ role?: "main" | "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor" | undefined;
101
+ reason?: string | undefined;
102
+ upstreamStatus?: number | undefined;
103
+ }>;
104
+ export type RoutingEvent = z.infer<typeof routingEventSchema>;
105
+ export declare const routingEventsSchema: z.ZodArray<z.ZodObject<{
106
+ at: z.ZodString;
107
+ requestId: z.ZodString;
108
+ requestedModel: z.ZodString;
109
+ resolvedModel: z.ZodOptional<z.ZodString>;
110
+ reportedModel: z.ZodOptional<z.ZodString>;
111
+ decision: z.ZodEnum<["allow", "alias", "reject", "fallback"]>;
112
+ role: z.ZodOptional<z.ZodEnum<["main", "subagent", "fast", "planning", "review", "summary", "compaction", "weak", "editor"]>>;
113
+ reason: z.ZodOptional<z.ZodString>;
114
+ upstreamStatus: z.ZodOptional<z.ZodNumber>;
115
+ }, "strict", z.ZodTypeAny, {
116
+ at: string;
117
+ requestId: string;
118
+ requestedModel: string;
119
+ decision: "allow" | "alias" | "reject" | "fallback";
120
+ resolvedModel?: string | undefined;
121
+ reportedModel?: string | undefined;
122
+ role?: "main" | "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor" | undefined;
123
+ reason?: string | undefined;
124
+ upstreamStatus?: number | undefined;
125
+ }, {
126
+ at: string;
127
+ requestId: string;
128
+ requestedModel: string;
129
+ decision: "allow" | "alias" | "reject" | "fallback";
130
+ resolvedModel?: string | undefined;
131
+ reportedModel?: string | undefined;
132
+ role?: "main" | "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor" | undefined;
133
+ reason?: string | undefined;
134
+ upstreamStatus?: number | undefined;
135
+ }>, "many">;
136
+ /** Stable object-key order; ordered fallback arrays retain their precedence. */
137
+ export declare function canonicalPolicyJSON(value: unknown): string;
@@ -0,0 +1,31 @@
1
+ import { type Model } from "./domain";
2
+ export declare const MODEL_POLICY_VERSION: 1;
3
+ export type ModelRole = "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor";
4
+ export type ModelPolicy = {
5
+ version?: 1;
6
+ roles?: Partial<Record<ModelRole, string>>;
7
+ allowedModels?: string[];
8
+ aliases?: Record<string, string>;
9
+ fallbacks?: Record<string, string[]>;
10
+ };
11
+ export type CompiledModelPolicy = {
12
+ version: 1;
13
+ model: string;
14
+ roles: Record<ModelRole, string>;
15
+ allowedModels: string[];
16
+ aliases: Record<string, string>;
17
+ fallbacks: Record<string, string[]>;
18
+ digest: string;
19
+ };
20
+ export type ModelGuidanceContext = {
21
+ harness: string;
22
+ providerId?: string;
23
+ baseUrl?: string;
24
+ model: string;
25
+ compiled: CompiledModelPolicy;
26
+ catalogPath?: string;
27
+ };
28
+ export declare function compileModelPolicy(model: string, catalog: readonly Model[], policy?: ModelPolicy): CompiledModelPolicy;
29
+ export declare function resolvePolicyModel(compiled: CompiledModelPolicy, requested: string): string;
30
+ export declare function renderModelGuidance(context: ModelGuidanceContext): string;
31
+ export declare function injectModelGuidance(protocol: "anthropic-messages" | "openai-chat" | "openai-responses" | "gemini-generate-content", body: unknown, guidance: string, operation?: string): unknown;
@@ -0,0 +1,23 @@
1
+ /** Pure compilation of the documented native model-policy surfaces.
2
+ *
3
+ * This module deliberately does not launch processes, read configuration, or
4
+ * infer provider aliases. Callers provide the exact model IDs they have
5
+ * already selected and may then serialize the returned env/config values.
6
+ */
7
+ export declare const nativeRoles: readonly ["main", "subagent", "fast", "planning", "review", "summary", "compaction", "weak", "editor"];
8
+ export type NativeRole = typeof nativeRoles[number];
9
+ export type NativePolicyHarness = "claude" | "codex" | "grok" | "opencode" | "opencode2" | "omp" | "hermes" | "aider" | "kilo" | "gemini" | "cline" | "dsh" | "pi" | "prime-agent";
10
+ export type NativeModelPolicyInput = {
11
+ harness: NativePolicyHarness;
12
+ mainModel: string;
13
+ roles?: Partial<Record<NativeRole, string>>;
14
+ version?: string;
15
+ };
16
+ export type NativeModelPolicy = {
17
+ harness: NativePolicyHarness;
18
+ env: Record<string, string>;
19
+ config: Record<string, unknown>;
20
+ unsupportedRoles: NativeRole[];
21
+ };
22
+ /** Compile exact role assignments into native settings/env without inventing knobs. */
23
+ export declare function compileNativeModelPolicy(input: NativeModelPolicyInput): NativeModelPolicy;
@@ -0,0 +1,33 @@
1
+ import type { ModelPolicy } from "./model-policy-schema";
2
+ type Dict = Record<string, unknown>;
3
+ export type OpenCodeRole = "main" | "subagent" | "planning" | "summary" | "compaction";
4
+ export type OpenCodeCompiledRoles = Partial<Record<OpenCodeRole, string>>;
5
+ export type PreservedOpenCodeAgent = Dict & {
6
+ name: string;
7
+ };
8
+ export type OpenCodeModelPolicyInput = {
9
+ providerId: string;
10
+ mainModel: string;
11
+ roles?: OpenCodeCompiledRoles | ModelPolicy["roles"];
12
+ preservedAgents?: PreservedOpenCodeAgent[];
13
+ /** OpenCode 2's native `agents` table or legacy OpenCode's `agent` table. */
14
+ format?: "v2" | "legacy";
15
+ };
16
+ export type OpenCodeModelPolicyResult = {
17
+ model: string;
18
+ agents: PreservedOpenCodeAgent[];
19
+ /** Only native OpenCode fields are emitted. */
20
+ config: {
21
+ model: string;
22
+ agents?: Record<string, Dict>;
23
+ agent?: Record<string, Dict>;
24
+ };
25
+ };
26
+ /**
27
+ * Merge Switcher routing into native OpenCode agent declarations. Prompts,
28
+ * permissions, modes and other native fields are copied without alteration.
29
+ */
30
+ export declare function compileOpenCodeModelPolicy(input: OpenCodeModelPolicyInput): OpenCodeModelPolicyResult;
31
+ /** Match OpenCode's explicit agent selection without interpreting prompt values as flags. */
32
+ export declare function openCodeInvocationModel(args: readonly string[], policy: OpenCodeModelPolicyResult, defaultAgent?: string): string;
33
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { PreparedLaunch } from "./harness-types";
2
+ /** Execute the verified native launch after Ori finishes its OpenRouter setup.
3
+ * Credentials are remapped from process environment, never written into the shim. */
4
+ export declare function prepareOriModelPolicy(target: "codex" | "grok", prepared: Pick<PreparedLaunch, "executable" | "args" | "env">): {
5
+ name: "codex" | "grok";
6
+ env: Record<string, string>;
7
+ script: string;
8
+ };