@hasna/switcher 0.1.0
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/Dockerfile +11 -0
- package/LICENSE +201 -0
- package/README.md +112 -0
- package/dist/catalog.d.ts +2 -0
- package/dist/cli/index.js +759 -0
- package/dist/cli.d.ts +2 -0
- package/dist/domain.d.ts +231 -0
- package/dist/generated/api.d.ts +1174 -0
- package/dist/harness-types.d.ts +24 -0
- package/dist/harnesses.d.ts +13 -0
- package/dist/http.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +227 -0
- package/dist/launcher.d.ts +9 -0
- package/dist/mcp/index.js +268 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/sdk.d.ts +311 -0
- package/dist/sdk.js +227 -0
- package/dist/serve/index.js +2800 -0
- package/dist/serve.d.ts +2 -0
- package/dist/service.d.ts +2 -0
- package/dist/store.d.ts +48 -0
- package/docker-compose.yml +41 -0
- package/hasna.contract.json +94 -0
- package/openapi.json +2202 -0
- package/package.json +70 -0
package/dist/sdk.d.ts
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import type { components } from "./generated/api";
|
|
2
|
+
export type ProviderInput = components["schemas"]["ProviderInput"];
|
|
3
|
+
export type Provider = components["schemas"]["Provider"];
|
|
4
|
+
export type ProfileInput = components["schemas"]["ProfileInput"];
|
|
5
|
+
export type Profile = components["schemas"]["Profile"];
|
|
6
|
+
export type Model = components["schemas"]["Model"];
|
|
7
|
+
export type Catalog = components["schemas"]["Catalog"];
|
|
8
|
+
export type LaunchPlan = components["schemas"]["LaunchPlan"];
|
|
9
|
+
export type Run = components["schemas"]["Run"];
|
|
10
|
+
export type Page<T> = {
|
|
11
|
+
data: T[];
|
|
12
|
+
total: number;
|
|
13
|
+
limit: number;
|
|
14
|
+
offset: number;
|
|
15
|
+
};
|
|
16
|
+
export declare class SwitcherError extends Error {
|
|
17
|
+
status: number;
|
|
18
|
+
code: string;
|
|
19
|
+
requestId?: string | undefined;
|
|
20
|
+
constructor(status: number, code: string, message: string, requestId?: string | undefined);
|
|
21
|
+
}
|
|
22
|
+
export type ClientOptions = {
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
apiKey: string | (() => string);
|
|
25
|
+
fetch?: typeof fetch;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
};
|
|
28
|
+
export declare class SwitcherClient {
|
|
29
|
+
private readonly options;
|
|
30
|
+
readonly baseUrl: string;
|
|
31
|
+
constructor(options: ClientOptions);
|
|
32
|
+
request<T>(method: string, path: string, body?: unknown, options?: {
|
|
33
|
+
version?: number;
|
|
34
|
+
idempotencyKey?: string;
|
|
35
|
+
}): Promise<T>;
|
|
36
|
+
private query;
|
|
37
|
+
listProviders(options?: {}): Promise<Page<{
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
baseUrl: string;
|
|
41
|
+
protocol: "anthropic-messages" | "openai-responses" | "openai-chat";
|
|
42
|
+
credentialEnv?: string;
|
|
43
|
+
authStyle: "bearer" | "x-api-key";
|
|
44
|
+
modelsPath: string;
|
|
45
|
+
manualModels: {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
contextWindow?: number;
|
|
50
|
+
maxOutputTokens?: number;
|
|
51
|
+
inputModalities?: string[];
|
|
52
|
+
outputModalities?: string[];
|
|
53
|
+
supportedParameters?: string[];
|
|
54
|
+
}[];
|
|
55
|
+
version: number;
|
|
56
|
+
updatedAt: string;
|
|
57
|
+
}>>;
|
|
58
|
+
getProvider(id: string): Promise<{
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
baseUrl: string;
|
|
62
|
+
protocol: "anthropic-messages" | "openai-responses" | "openai-chat";
|
|
63
|
+
credentialEnv?: string;
|
|
64
|
+
authStyle: "bearer" | "x-api-key";
|
|
65
|
+
modelsPath: string;
|
|
66
|
+
manualModels: {
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
contextWindow?: number;
|
|
71
|
+
maxOutputTokens?: number;
|
|
72
|
+
inputModalities?: string[];
|
|
73
|
+
outputModalities?: string[];
|
|
74
|
+
supportedParameters?: string[];
|
|
75
|
+
}[];
|
|
76
|
+
version: number;
|
|
77
|
+
updatedAt: string;
|
|
78
|
+
}>;
|
|
79
|
+
createProvider(input: ProviderInput, idempotencyKey?: string): Promise<{
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
baseUrl: string;
|
|
83
|
+
protocol: "anthropic-messages" | "openai-responses" | "openai-chat";
|
|
84
|
+
credentialEnv?: string;
|
|
85
|
+
authStyle: "bearer" | "x-api-key";
|
|
86
|
+
modelsPath: string;
|
|
87
|
+
manualModels: {
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
description?: string;
|
|
91
|
+
contextWindow?: number;
|
|
92
|
+
maxOutputTokens?: number;
|
|
93
|
+
inputModalities?: string[];
|
|
94
|
+
outputModalities?: string[];
|
|
95
|
+
supportedParameters?: string[];
|
|
96
|
+
}[];
|
|
97
|
+
version: number;
|
|
98
|
+
updatedAt: string;
|
|
99
|
+
}>;
|
|
100
|
+
updateProvider(input: ProviderInput, version: number, idempotencyKey?: string): Promise<{
|
|
101
|
+
id: string;
|
|
102
|
+
name: string;
|
|
103
|
+
baseUrl: string;
|
|
104
|
+
protocol: "anthropic-messages" | "openai-responses" | "openai-chat";
|
|
105
|
+
credentialEnv?: string;
|
|
106
|
+
authStyle: "bearer" | "x-api-key";
|
|
107
|
+
modelsPath: string;
|
|
108
|
+
manualModels: {
|
|
109
|
+
id: string;
|
|
110
|
+
name: string;
|
|
111
|
+
description?: string;
|
|
112
|
+
contextWindow?: number;
|
|
113
|
+
maxOutputTokens?: number;
|
|
114
|
+
inputModalities?: string[];
|
|
115
|
+
outputModalities?: string[];
|
|
116
|
+
supportedParameters?: string[];
|
|
117
|
+
}[];
|
|
118
|
+
version: number;
|
|
119
|
+
updatedAt: string;
|
|
120
|
+
}>;
|
|
121
|
+
deleteProvider(id: string, version: number, idempotencyKey?: string): Promise<{
|
|
122
|
+
deleted: string;
|
|
123
|
+
}>;
|
|
124
|
+
refreshModels(id: string, idempotencyKey?: string): Promise<{
|
|
125
|
+
models: {
|
|
126
|
+
id: string;
|
|
127
|
+
name: string;
|
|
128
|
+
description?: string;
|
|
129
|
+
contextWindow?: number;
|
|
130
|
+
maxOutputTokens?: number;
|
|
131
|
+
inputModalities?: string[];
|
|
132
|
+
outputModalities?: string[];
|
|
133
|
+
supportedParameters?: string[];
|
|
134
|
+
}[];
|
|
135
|
+
refreshedAt: string;
|
|
136
|
+
source: "remote" | "manual";
|
|
137
|
+
}>;
|
|
138
|
+
listModels(id: string, options?: {}): Promise<{
|
|
139
|
+
data: {
|
|
140
|
+
id: string;
|
|
141
|
+
name: string;
|
|
142
|
+
description?: string;
|
|
143
|
+
contextWindow?: number;
|
|
144
|
+
maxOutputTokens?: number;
|
|
145
|
+
inputModalities?: string[];
|
|
146
|
+
outputModalities?: string[];
|
|
147
|
+
supportedParameters?: string[];
|
|
148
|
+
codingEligible: boolean;
|
|
149
|
+
}[];
|
|
150
|
+
total: number;
|
|
151
|
+
limit: number;
|
|
152
|
+
offset: number;
|
|
153
|
+
refreshedAt: string;
|
|
154
|
+
source: "remote" | "manual";
|
|
155
|
+
}>;
|
|
156
|
+
listProfiles(options?: {}): Promise<Page<{
|
|
157
|
+
id: string;
|
|
158
|
+
name: string;
|
|
159
|
+
providerId: string;
|
|
160
|
+
harness: "claude" | "codex" | "grok" | "opencode2";
|
|
161
|
+
model: string;
|
|
162
|
+
version: number;
|
|
163
|
+
updatedAt: string;
|
|
164
|
+
}>>;
|
|
165
|
+
getProfile(id: string): Promise<{
|
|
166
|
+
id: string;
|
|
167
|
+
name: string;
|
|
168
|
+
providerId: string;
|
|
169
|
+
harness: "claude" | "codex" | "grok" | "opencode2";
|
|
170
|
+
model: string;
|
|
171
|
+
version: number;
|
|
172
|
+
updatedAt: string;
|
|
173
|
+
}>;
|
|
174
|
+
createProfile(input: ProfileInput, idempotencyKey?: string): Promise<{
|
|
175
|
+
id: string;
|
|
176
|
+
name: string;
|
|
177
|
+
providerId: string;
|
|
178
|
+
harness: "claude" | "codex" | "grok" | "opencode2";
|
|
179
|
+
model: string;
|
|
180
|
+
version: number;
|
|
181
|
+
updatedAt: string;
|
|
182
|
+
}>;
|
|
183
|
+
updateProfile(input: ProfileInput, version: number, idempotencyKey?: string): Promise<{
|
|
184
|
+
id: string;
|
|
185
|
+
name: string;
|
|
186
|
+
providerId: string;
|
|
187
|
+
harness: "claude" | "codex" | "grok" | "opencode2";
|
|
188
|
+
model: string;
|
|
189
|
+
version: number;
|
|
190
|
+
updatedAt: string;
|
|
191
|
+
}>;
|
|
192
|
+
deleteProfile(id: string, version: number, idempotencyKey?: string): Promise<{
|
|
193
|
+
deleted: string;
|
|
194
|
+
}>;
|
|
195
|
+
launchPlan(profileId: string, idempotencyKey?: string): Promise<{
|
|
196
|
+
provider: {
|
|
197
|
+
id: string;
|
|
198
|
+
name: string;
|
|
199
|
+
baseUrl: string;
|
|
200
|
+
protocol: "anthropic-messages" | "openai-responses" | "openai-chat";
|
|
201
|
+
credentialEnv?: string;
|
|
202
|
+
authStyle: "bearer" | "x-api-key";
|
|
203
|
+
modelsPath: string;
|
|
204
|
+
manualModels: {
|
|
205
|
+
id: string;
|
|
206
|
+
name: string;
|
|
207
|
+
description?: string;
|
|
208
|
+
contextWindow?: number;
|
|
209
|
+
maxOutputTokens?: number;
|
|
210
|
+
inputModalities?: string[];
|
|
211
|
+
outputModalities?: string[];
|
|
212
|
+
supportedParameters?: string[];
|
|
213
|
+
}[];
|
|
214
|
+
version: number;
|
|
215
|
+
updatedAt: string;
|
|
216
|
+
};
|
|
217
|
+
profile: {
|
|
218
|
+
id: string;
|
|
219
|
+
name: string;
|
|
220
|
+
providerId: string;
|
|
221
|
+
harness: "claude" | "codex" | "grok" | "opencode2";
|
|
222
|
+
model: string;
|
|
223
|
+
version: number;
|
|
224
|
+
updatedAt: string;
|
|
225
|
+
};
|
|
226
|
+
catalog: {
|
|
227
|
+
models: {
|
|
228
|
+
id: string;
|
|
229
|
+
name: string;
|
|
230
|
+
description?: string;
|
|
231
|
+
contextWindow?: number;
|
|
232
|
+
maxOutputTokens?: number;
|
|
233
|
+
inputModalities?: string[];
|
|
234
|
+
outputModalities?: string[];
|
|
235
|
+
supportedParameters?: string[];
|
|
236
|
+
}[];
|
|
237
|
+
refreshedAt: string;
|
|
238
|
+
source: "remote" | "manual";
|
|
239
|
+
};
|
|
240
|
+
planToken: string;
|
|
241
|
+
warnings: string[];
|
|
242
|
+
}>;
|
|
243
|
+
listRuns(options?: {}): Promise<Page<{
|
|
244
|
+
profileId: string;
|
|
245
|
+
harness: "claude" | "codex" | "grok" | "opencode2";
|
|
246
|
+
model: string;
|
|
247
|
+
planToken: string;
|
|
248
|
+
version: number;
|
|
249
|
+
updatedAt: string;
|
|
250
|
+
providerId: string;
|
|
251
|
+
providerVersion: number;
|
|
252
|
+
profileVersion: number;
|
|
253
|
+
id: string;
|
|
254
|
+
status: "running" | "exited" | "failed" | "interrupted";
|
|
255
|
+
startedAt: string;
|
|
256
|
+
endedAt?: string;
|
|
257
|
+
exitCode?: number;
|
|
258
|
+
}>>;
|
|
259
|
+
getRun(id: string): Promise<{
|
|
260
|
+
profileId: string;
|
|
261
|
+
harness: "claude" | "codex" | "grok" | "opencode2";
|
|
262
|
+
model: string;
|
|
263
|
+
planToken: string;
|
|
264
|
+
version: number;
|
|
265
|
+
updatedAt: string;
|
|
266
|
+
providerId: string;
|
|
267
|
+
providerVersion: number;
|
|
268
|
+
profileVersion: number;
|
|
269
|
+
id: string;
|
|
270
|
+
status: "running" | "exited" | "failed" | "interrupted";
|
|
271
|
+
startedAt: string;
|
|
272
|
+
endedAt?: string;
|
|
273
|
+
exitCode?: number;
|
|
274
|
+
}>;
|
|
275
|
+
createRun(input: components["schemas"]["RunInput"], idempotencyKey?: string): Promise<{
|
|
276
|
+
profileId: string;
|
|
277
|
+
harness: "claude" | "codex" | "grok" | "opencode2";
|
|
278
|
+
model: string;
|
|
279
|
+
planToken: string;
|
|
280
|
+
version: number;
|
|
281
|
+
updatedAt: string;
|
|
282
|
+
providerId: string;
|
|
283
|
+
providerVersion: number;
|
|
284
|
+
profileVersion: number;
|
|
285
|
+
id: string;
|
|
286
|
+
status: "running" | "exited" | "failed" | "interrupted";
|
|
287
|
+
startedAt: string;
|
|
288
|
+
endedAt?: string;
|
|
289
|
+
exitCode?: number;
|
|
290
|
+
}>;
|
|
291
|
+
finishRun(id: string, version: number, input: {
|
|
292
|
+
status: "exited" | "failed" | "interrupted";
|
|
293
|
+
exitCode: number;
|
|
294
|
+
}, idempotencyKey?: string): Promise<{
|
|
295
|
+
profileId: string;
|
|
296
|
+
harness: "claude" | "codex" | "grok" | "opencode2";
|
|
297
|
+
model: string;
|
|
298
|
+
planToken: string;
|
|
299
|
+
version: number;
|
|
300
|
+
updatedAt: string;
|
|
301
|
+
providerId: string;
|
|
302
|
+
providerVersion: number;
|
|
303
|
+
profileVersion: number;
|
|
304
|
+
id: string;
|
|
305
|
+
status: "running" | "exited" | "failed" | "interrupted";
|
|
306
|
+
startedAt: string;
|
|
307
|
+
endedAt?: string;
|
|
308
|
+
exitCode?: number;
|
|
309
|
+
}>;
|
|
310
|
+
}
|
|
311
|
+
export declare function clientFromEnv(env?: Record<string, string | undefined>): SwitcherClient;
|
package/dist/sdk.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// src/domain.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var harnessSchema = z.enum(["claude", "codex", "grok", "opencode2"]);
|
|
4
|
+
var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat"]);
|
|
5
|
+
var idSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
|
|
6
|
+
var label = z.string().min(1).max(200);
|
|
7
|
+
var envRef = z.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
|
|
8
|
+
function endpoint(value) {
|
|
9
|
+
let url;
|
|
10
|
+
try {
|
|
11
|
+
url = new URL(value);
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Fault(400, "invalid_url", "Use an absolute HTTPS URL (HTTP is allowed on loopback).");
|
|
14
|
+
}
|
|
15
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
16
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local) || url.username || url.password || url.search || url.hash)
|
|
17
|
+
throw new Fault(400, "invalid_url", "URL must use HTTPS, contain no credentials/query/fragment, or use HTTP on loopback.");
|
|
18
|
+
return url.href.replace(/\/+$/, "");
|
|
19
|
+
}
|
|
20
|
+
var urlSchema = z.string().max(2000).superRefine((v, ctx) => {
|
|
21
|
+
try {
|
|
22
|
+
endpoint(v);
|
|
23
|
+
} catch {
|
|
24
|
+
ctx.addIssue({ code: "custom", message: "Invalid endpoint URL" });
|
|
25
|
+
}
|
|
26
|
+
}).transform(endpoint);
|
|
27
|
+
var modelSchema = z.object({
|
|
28
|
+
id: z.string().min(1).max(300),
|
|
29
|
+
name: label,
|
|
30
|
+
description: z.string().max(8000).optional(),
|
|
31
|
+
contextWindow: z.number().int().positive().optional(),
|
|
32
|
+
maxOutputTokens: z.number().int().positive().optional(),
|
|
33
|
+
inputModalities: z.array(z.string().max(50)).max(20).optional(),
|
|
34
|
+
outputModalities: z.array(z.string().max(50)).max(20).optional(),
|
|
35
|
+
supportedParameters: z.array(z.string().max(100)).max(100).optional()
|
|
36
|
+
}).strict();
|
|
37
|
+
var providerInputSchema = z.object({
|
|
38
|
+
id: idSchema,
|
|
39
|
+
name: label,
|
|
40
|
+
baseUrl: urlSchema,
|
|
41
|
+
protocol: protocolSchema,
|
|
42
|
+
credentialEnv: envRef.optional(),
|
|
43
|
+
authStyle: z.enum(["bearer", "x-api-key"]).default("bearer"),
|
|
44
|
+
modelsPath: z.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
|
|
45
|
+
manualModels: z.array(modelSchema).max(1e4).default([])
|
|
46
|
+
}).strict().refine((p) => !p.modelsPath.split("/").includes("..") && !p.modelsPath.startsWith("/"), "modelsPath must be relative");
|
|
47
|
+
var profileInputSchema = z.object({
|
|
48
|
+
id: idSchema,
|
|
49
|
+
name: label,
|
|
50
|
+
providerId: idSchema,
|
|
51
|
+
harness: harnessSchema,
|
|
52
|
+
model: z.string().min(1).max(300)
|
|
53
|
+
}).strict();
|
|
54
|
+
var runInputSchema = z.object({
|
|
55
|
+
profileId: idSchema,
|
|
56
|
+
harness: harnessSchema,
|
|
57
|
+
model: z.string().min(1).max(300),
|
|
58
|
+
planToken: z.string().regex(/^[a-f0-9]{64}$/)
|
|
59
|
+
}).strict();
|
|
60
|
+
var runUpdateSchema = z.object({
|
|
61
|
+
status: z.enum(["exited", "failed", "interrupted"]),
|
|
62
|
+
exitCode: z.number().int().min(0).max(255)
|
|
63
|
+
}).strict();
|
|
64
|
+
|
|
65
|
+
class Fault extends Error {
|
|
66
|
+
status;
|
|
67
|
+
code;
|
|
68
|
+
constructor(status, code, message) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.status = status;
|
|
71
|
+
this.code = code;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/http.ts
|
|
76
|
+
var MAX_BYTES = 16 * 1024 * 1024;
|
|
77
|
+
async function boundedJson(response, maxBytes = MAX_BYTES) {
|
|
78
|
+
if (!response.body)
|
|
79
|
+
throw new Fault(502, "invalid_upstream", "Upstream returned no body.");
|
|
80
|
+
const reader = response.body.getReader();
|
|
81
|
+
const chunks = [];
|
|
82
|
+
let size = 0;
|
|
83
|
+
try {
|
|
84
|
+
while (true) {
|
|
85
|
+
const item = await reader.read();
|
|
86
|
+
if (item.done)
|
|
87
|
+
break;
|
|
88
|
+
size += item.value.byteLength;
|
|
89
|
+
if (size > maxBytes)
|
|
90
|
+
throw new Fault(502, "response_too_large", "Response exceeds the size limit.");
|
|
91
|
+
chunks.push(item.value);
|
|
92
|
+
}
|
|
93
|
+
const bytes = new Uint8Array(size);
|
|
94
|
+
let offset = 0;
|
|
95
|
+
for (const chunk of chunks) {
|
|
96
|
+
bytes.set(chunk, offset);
|
|
97
|
+
offset += chunk.length;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
101
|
+
} catch {
|
|
102
|
+
throw new Fault(502, "invalid_upstream", "Upstream returned invalid JSON.");
|
|
103
|
+
}
|
|
104
|
+
} finally {
|
|
105
|
+
await reader.cancel().catch(() => {});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/sdk.ts
|
|
110
|
+
import { resolveCredential } from "@hasna/contracts/client";
|
|
111
|
+
|
|
112
|
+
class SwitcherError extends Error {
|
|
113
|
+
status;
|
|
114
|
+
code;
|
|
115
|
+
requestId;
|
|
116
|
+
constructor(status, code, message, requestId) {
|
|
117
|
+
super(message);
|
|
118
|
+
this.status = status;
|
|
119
|
+
this.code = code;
|
|
120
|
+
this.requestId = requestId;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
class SwitcherClient {
|
|
125
|
+
options;
|
|
126
|
+
baseUrl;
|
|
127
|
+
constructor(options) {
|
|
128
|
+
this.baseUrl = endpoint(options.baseUrl).replace(/\/v1$/, "");
|
|
129
|
+
if (typeof options.apiKey === "string" && (!options.apiKey || /[\r\n]/.test(options.apiKey)))
|
|
130
|
+
throw new Error("Switcher API key is required.");
|
|
131
|
+
this.options = { ...options };
|
|
132
|
+
}
|
|
133
|
+
async request(method, path, body, options = {}) {
|
|
134
|
+
if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) || path.includes(".."))
|
|
135
|
+
throw new Error("Invalid API path.");
|
|
136
|
+
const apiKey = typeof this.options.apiKey === "function" ? this.options.apiKey() : this.options.apiKey;
|
|
137
|
+
if (!apiKey || /[\r\n]/.test(apiKey))
|
|
138
|
+
throw new Error("Switcher API key is required.");
|
|
139
|
+
const headers = { authorization: `Bearer ${apiKey}`, accept: "application/json" };
|
|
140
|
+
if (body !== undefined)
|
|
141
|
+
headers["content-type"] = "application/json";
|
|
142
|
+
if (method !== "GET")
|
|
143
|
+
headers["idempotency-key"] = options.idempotencyKey ?? crypto.randomUUID();
|
|
144
|
+
if (options.version !== undefined)
|
|
145
|
+
headers["if-match"] = String(options.version);
|
|
146
|
+
let response;
|
|
147
|
+
try {
|
|
148
|
+
response = await (this.options.fetch ?? fetch)(`${this.baseUrl}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), redirect: "manual", signal: AbortSignal.timeout(this.options.timeoutMs ?? 120000) });
|
|
149
|
+
} catch {
|
|
150
|
+
throw new SwitcherError(0, "connection_failed", "Switcher API request failed; check endpoint and service availability.");
|
|
151
|
+
}
|
|
152
|
+
let data;
|
|
153
|
+
try {
|
|
154
|
+
data = await boundedJson(response);
|
|
155
|
+
} catch {
|
|
156
|
+
throw new SwitcherError(response.status, "invalid_response", "Switcher API returned invalid JSON.");
|
|
157
|
+
}
|
|
158
|
+
if (!response.ok)
|
|
159
|
+
throw new SwitcherError(response.status, data?.error?.code ?? "api_error", data?.error?.message ?? `Switcher API returned HTTP ${response.status}.`, data?.error?.requestId);
|
|
160
|
+
return data;
|
|
161
|
+
}
|
|
162
|
+
query(options = {}) {
|
|
163
|
+
return new URLSearchParams(Object.entries(options).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)])).toString();
|
|
164
|
+
}
|
|
165
|
+
listProviders(options = {}) {
|
|
166
|
+
return this.request("GET", `/v1/providers?${this.query(options)}`);
|
|
167
|
+
}
|
|
168
|
+
getProvider(id) {
|
|
169
|
+
return this.request("GET", `/v1/providers/${encodeURIComponent(id)}`);
|
|
170
|
+
}
|
|
171
|
+
createProvider(input, idempotencyKey) {
|
|
172
|
+
return this.request("POST", "/v1/providers", input, { idempotencyKey });
|
|
173
|
+
}
|
|
174
|
+
updateProvider(input, version, idempotencyKey) {
|
|
175
|
+
return this.request("PUT", `/v1/providers/${encodeURIComponent(input.id)}`, input, { version, idempotencyKey });
|
|
176
|
+
}
|
|
177
|
+
deleteProvider(id, version, idempotencyKey) {
|
|
178
|
+
return this.request("DELETE", `/v1/providers/${encodeURIComponent(id)}`, undefined, { version, idempotencyKey });
|
|
179
|
+
}
|
|
180
|
+
refreshModels(id, idempotencyKey) {
|
|
181
|
+
return this.request("POST", `/v1/providers/${encodeURIComponent(id)}/refresh`, {}, { idempotencyKey });
|
|
182
|
+
}
|
|
183
|
+
listModels(id, options = {}) {
|
|
184
|
+
return this.request("GET", `/v1/providers/${encodeURIComponent(id)}/models?${this.query(options)}`);
|
|
185
|
+
}
|
|
186
|
+
listProfiles(options = {}) {
|
|
187
|
+
return this.request("GET", `/v1/profiles?${this.query(options)}`);
|
|
188
|
+
}
|
|
189
|
+
getProfile(id) {
|
|
190
|
+
return this.request("GET", `/v1/profiles/${encodeURIComponent(id)}`);
|
|
191
|
+
}
|
|
192
|
+
createProfile(input, idempotencyKey) {
|
|
193
|
+
return this.request("POST", "/v1/profiles", input, { idempotencyKey });
|
|
194
|
+
}
|
|
195
|
+
updateProfile(input, version, idempotencyKey) {
|
|
196
|
+
return this.request("PUT", `/v1/profiles/${encodeURIComponent(input.id)}`, input, { version, idempotencyKey });
|
|
197
|
+
}
|
|
198
|
+
deleteProfile(id, version, idempotencyKey) {
|
|
199
|
+
return this.request("DELETE", `/v1/profiles/${encodeURIComponent(id)}`, undefined, { version, idempotencyKey });
|
|
200
|
+
}
|
|
201
|
+
launchPlan(profileId, idempotencyKey) {
|
|
202
|
+
return this.request("POST", "/v1/launch-plans", { profileId }, { idempotencyKey });
|
|
203
|
+
}
|
|
204
|
+
listRuns(options = {}) {
|
|
205
|
+
return this.request("GET", `/v1/runs?${this.query(options)}`);
|
|
206
|
+
}
|
|
207
|
+
getRun(id) {
|
|
208
|
+
return this.request("GET", `/v1/runs/${encodeURIComponent(id)}`);
|
|
209
|
+
}
|
|
210
|
+
createRun(input, idempotencyKey) {
|
|
211
|
+
return this.request("POST", "/v1/runs", input, { idempotencyKey });
|
|
212
|
+
}
|
|
213
|
+
finishRun(id, version, input, idempotencyKey) {
|
|
214
|
+
return this.request("PATCH", `/v1/runs/${encodeURIComponent(id)}`, input, { version, idempotencyKey });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function clientFromEnv(env = process.env) {
|
|
218
|
+
const credential = () => resolveCredential("switcher", Object.fromEntries(Object.entries(env).filter(([name]) => name === "HASNA_SWITCHER_API_KEY")), { keychain: { enabled: false } })?.apiKey ?? "";
|
|
219
|
+
if (!env.HASNA_SWITCHER_API_URL || !credential())
|
|
220
|
+
throw new Error("Set HASNA_SWITCHER_API_URL and HASNA_SWITCHER_API_KEY; no local database fallback is available.");
|
|
221
|
+
return new SwitcherClient({ baseUrl: env.HASNA_SWITCHER_API_URL, apiKey: credential });
|
|
222
|
+
}
|
|
223
|
+
export {
|
|
224
|
+
clientFromEnv,
|
|
225
|
+
SwitcherError,
|
|
226
|
+
SwitcherClient
|
|
227
|
+
};
|