@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/sdk.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
|