@amir83nasr/10router 0.1.16 → 0.1.18
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +5 -5
- package/.next/build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/(dashboard)/dashboard/cli-tools/[toolId]/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/cli-tools/[toolId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/cli-tools/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/combos/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/console-log/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/console-log/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/endpoint/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/[id]/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/profile/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/profile/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/providers/[id]/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/providers/new/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/providers/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/proxy-pools/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/proxy-pools/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/pxpipe/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/pxpipe/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/quota/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/quota/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/token-saver/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/token-saver/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/usage/page.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/usage/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +5 -5
- package/.next/server/app/_not-found.segments/_full.segment.rsc +5 -5
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/cli-tools/all-statuses/route.js +1 -1
- package/.next/server/app/api/cli-tools/claude-settings/route.js +1 -1
- package/.next/server/app/api/cli-tools/codex-settings/route.js +1 -1
- package/.next/server/app/api/cli-tools/cowork-mcp-registry/route.js +1 -1
- package/.next/server/app/api/cli-tools/cowork-mcp-tools/route.js +1 -1
- package/.next/server/app/api/cli-tools/cowork-settings/route.js +1 -1
- package/.next/server/app/api/cli-tools/dsh-settings/route.js +1 -1
- package/.next/server/app/api/cli-tools/opencode-settings/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/oauth/[provider]/[action]/route.js +1 -1
- package/.next/server/app/api/oauth/codex/bulk-import/route.js +1 -1
- package/.next/server/app/api/oauth/codex/import-token/route.js +1 -1
- package/.next/server/app/api/providers/[id]/models/route.js +1 -1
- package/.next/server/app/api/providers/[id]/test/route.js +1 -1
- package/.next/server/app/api/providers/client/route.js +1 -1
- package/.next/server/app/api/providers/route.js +1 -1
- package/.next/server/app/api/providers/test-batch/route.js +1 -1
- package/.next/server/app/api/providers/validate/route.js +1 -1
- package/.next/server/app/api/settings/route.js +1 -1
- package/.next/server/app/api/translator/console-logs/route.js +1 -1
- package/.next/server/app/api/translator/console-logs/stream/route.js +1 -1
- package/.next/server/app/api/translator/send/route.js +1 -1
- package/.next/server/app/api/translator/translate/route.js +1 -1
- package/.next/server/app/api/tunnel/disable/route.js +1 -1
- package/.next/server/app/api/tunnel/enable/route.js +1 -1
- package/.next/server/app/api/tunnel/tailscale-disable/route.js +1 -1
- package/.next/server/app/api/tunnel/tailscale-enable/route.js +1 -1
- package/.next/server/app/api/tunnel/tailscale-install/route.js +2 -2
- package/.next/server/app/api/usage/[connectionId]/codex-reset-credits/route.js +1 -1
- package/.next/server/app/api/usage/[connectionId]/route.js +1 -1
- package/.next/server/app/api/v1/api/chat/route.js +1 -1
- package/.next/server/app/api/v1/audio/speech/route.js +1 -1
- package/.next/server/app/api/v1/audio/transcriptions/route.js +1 -1
- package/.next/server/app/api/v1/chat/completions/route.js +1 -1
- package/.next/server/app/api/v1/embeddings/route.js +1 -1
- package/.next/server/app/api/v1/images/generations/route.js +1 -1
- package/.next/server/app/api/v1/messages/route.js +1 -1
- package/.next/server/app/api/v1/models/[...model]/route.js +1 -1
- package/.next/server/app/api/v1/models/route.js +1 -1
- package/.next/server/app/api/v1/responses/compact/route.js +1 -1
- package/.next/server/app/api/v1/responses/route.js +1 -1
- package/.next/server/app/api/v1/route.js +1 -1
- package/.next/server/app/api/v1/search/route.js +1 -1
- package/.next/server/app/api/v1/systemone/route.js +1 -1
- package/.next/server/app/api/v1/videos/[id]/route.js +1 -1
- package/.next/server/app/api/v1/videos/edits/route.js +1 -1
- package/.next/server/app/api/v1/videos/extensions/route.js +1 -1
- package/.next/server/app/api/v1/videos/generations/route.js +1 -1
- package/.next/server/app/api/v1/web/fetch/route.js +1 -1
- package/.next/server/app/api/v1beta/models/[...path]/route.js +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/callback/page.js +1 -1
- package/.next/server/app/callback/page_client-reference-manifest.js +1 -1
- package/.next/server/app/callback.html +1 -1
- package/.next/server/app/callback.rsc +5 -5
- package/.next/server/app/callback.segments/_full.segment.rsc +5 -5
- package/.next/server/app/callback.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/callback.segments/callback/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/dashboard/cli-tools.html +1 -1
- package/.next/server/app/dashboard/cli-tools.rsc +8 -8
- package/.next/server/app/dashboard/cli-tools.segments/!KGRhc2hib2FyZCk/dashboard/cli-tools/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/cli-tools.segments/_full.segment.rsc +8 -8
- package/.next/server/app/dashboard/cli-tools.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/combos.html +1 -1
- package/.next/server/app/dashboard/combos.rsc +7 -7
- package/.next/server/app/dashboard/combos.segments/!KGRhc2hib2FyZCk/dashboard/combos/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/combos.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/combos.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/endpoint.html +1 -1
- package/.next/server/app/dashboard/endpoint.rsc +8 -8
- package/.next/server/app/dashboard/endpoint.segments/!KGRhc2hib2FyZCk/dashboard/endpoint/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/endpoint.segments/_full.segment.rsc +8 -8
- package/.next/server/app/dashboard/endpoint.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/media-providers/systemone.html +1 -1
- package/.next/server/app/dashboard/media-providers/systemone.rsc +7 -7
- package/.next/server/app/dashboard/media-providers/systemone.segments/!KGRhc2hib2FyZCk/dashboard/media-providers/systemone/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/media-providers/systemone.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/media-providers/systemone.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/profile.html +1 -1
- package/.next/server/app/dashboard/profile.rsc +7 -7
- package/.next/server/app/dashboard/profile.segments/!KGRhc2hib2FyZCk/dashboard/profile/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/profile.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/profile.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/providers/new.html +1 -1
- package/.next/server/app/dashboard/providers/new.rsc +7 -7
- package/.next/server/app/dashboard/providers/new.segments/!KGRhc2hib2FyZCk/dashboard/providers/new/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/providers/new.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/providers/new.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/providers.html +1 -1
- package/.next/server/app/dashboard/providers.rsc +7 -7
- package/.next/server/app/dashboard/providers.segments/!KGRhc2hib2FyZCk/dashboard/providers/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/providers.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/providers.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/proxy-pools.html +1 -1
- package/.next/server/app/dashboard/proxy-pools.rsc +7 -7
- package/.next/server/app/dashboard/proxy-pools.segments/!KGRhc2hib2FyZCk/dashboard/proxy-pools/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/proxy-pools.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/proxy-pools.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/pxpipe.html +1 -1
- package/.next/server/app/dashboard/pxpipe.rsc +6 -6
- package/.next/server/app/dashboard/pxpipe.segments/!KGRhc2hib2FyZCk/dashboard/pxpipe/__PAGE__.segment.rsc +6 -6
- package/.next/server/app/dashboard/pxpipe.segments/_full.segment.rsc +6 -6
- package/.next/server/app/dashboard/pxpipe.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/quota.html +2 -2
- package/.next/server/app/dashboard/quota.rsc +8 -8
- package/.next/server/app/dashboard/quota.segments/!KGRhc2hib2FyZCk/dashboard/quota/__PAGE__.segment.rsc +8 -8
- package/.next/server/app/dashboard/quota.segments/_full.segment.rsc +8 -8
- package/.next/server/app/dashboard/quota.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/settings/pricing/page.js +1 -1
- package/.next/server/app/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
- package/.next/server/app/dashboard/settings/pricing.html +1 -1
- package/.next/server/app/dashboard/settings/pricing.rsc +5 -5
- package/.next/server/app/dashboard/settings/pricing.segments/_full.segment.rsc +5 -5
- package/.next/server/app/dashboard/settings/pricing.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/settings/pricing.segments/dashboard/settings/pricing/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/dashboard/token-saver.html +1 -1
- package/.next/server/app/dashboard/token-saver.rsc +6 -6
- package/.next/server/app/dashboard/token-saver.segments/!KGRhc2hib2FyZCk/dashboard/token-saver/__PAGE__.segment.rsc +6 -6
- package/.next/server/app/dashboard/token-saver.segments/_full.segment.rsc +6 -6
- package/.next/server/app/dashboard/token-saver.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/usage.html +2 -2
- package/.next/server/app/dashboard/usage.rsc +7 -7
- package/.next/server/app/dashboard/usage.segments/!KGRhc2hib2FyZCk/dashboard/usage/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/usage.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/usage.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard.html +1 -1
- package/.next/server/app/dashboard.rsc +8 -8
- package/.next/server/app/dashboard.segments/!KGRhc2hib2FyZCk/dashboard/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard.segments/_full.segment.rsc +8 -8
- package/.next/server/app/dashboard.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/landing/page.js +1 -1
- package/.next/server/app/landing/page_client-reference-manifest.js +1 -1
- package/.next/server/app/landing.html +1 -1
- package/.next/server/app/landing.rsc +5 -5
- package/.next/server/app/landing.segments/_full.segment.rsc +5 -5
- package/.next/server/app/landing.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/landing.segments/landing/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.rsc +5 -5
- package/.next/server/app/login.segments/_full.segment.rsc +5 -5
- package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +5 -5
- package/.next/server/chunks/1003.js +1 -1
- package/.next/server/chunks/1649.js +1 -1
- package/.next/server/chunks/1751.js +1 -1
- package/.next/server/chunks/2143.js +1 -1
- package/.next/server/chunks/2151.js +1 -1
- package/.next/server/chunks/2282.js +3 -3
- package/.next/server/chunks/2531.js +1 -1
- package/.next/server/chunks/2801.js +1 -1
- package/.next/server/chunks/3671.js +1 -1
- package/.next/server/chunks/6343.js +3 -3
- package/.next/server/chunks/6617.js +1 -1
- package/.next/server/chunks/7591.js +1 -1
- package/.next/server/chunks/7904.js +1 -1
- package/.next/server/chunks/9429.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/1660-084df607ff4f889d.js +1 -0
- package/.next/static/chunks/{6770-e2921b2ce695ed57.js → 6770-9e948c92e87956c4.js} +1 -1
- package/CHANGELOG.md +11 -0
- package/open-sse/config/opencodeGo.js +3 -2
- package/open-sse/executors/opencode-go.js +7 -0
- package/open-sse/executors/opencode.js +20 -0
- package/open-sse/handlers/chatCore.js +7 -14
- package/open-sse/providers/capabilities.js +30 -0
- package/open-sse/providers/models/helpers.js +14 -0
- package/open-sse/providers/registry/opencode-go.js +116 -31
- package/open-sse/services/lazyRuntime.js +110 -0
- package/open-sse/services/opencodeTlsClient.js +91 -0
- package/open-sse/services/provider.js +27 -0
- package/open-sse/utils/writableDataDir.js +32 -0
- package/package.json +1 -1
- package/.next/static/chunks/1660-c4c1cfa3cb7c9b2e.js +0 -1
- /package/.next/static/{EsP8l_VAPRRfBiiJPJqQe → PyHCF87rmgiD0pV5fDUOP}/_buildManifest.js +0 -0
- /package/.next/static/{EsP8l_VAPRRfBiiJPJqQe → PyHCF87rmgiD0pV5fDUOP}/_ssgManifest.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to 10Router will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.18] — 2026-10-30
|
|
6
|
+
|
|
7
|
+
- Synced OpenCode free-tier and OpenCode Go improvements from 9router: TLS-impersonating client (wreq-js) for the free tier, opencode free-tier capability entries (glm-5.3-flash-free on openai format, mimo reasoning, deepseek-v4.1-flash-free 330k context cap), and `isOpencodeFreeTierModel`/`isDeepSeekModel` helpers.
|
|
8
|
+
|
|
9
|
+
## [0.1.17] — 2026-10-26
|
|
10
|
+
|
|
11
|
+
- Fix: OpenCode Go routed each model to the provider's default endpoint instead of one it actually serves. An OpenAI-format client asking for a claude-only model (`claude-haiku-5-5`, `minimax-m2.7`) was sent to `/chat/completions` and rejected upstream with `ModelProtocolUnsupported`; `chatCore` and the tests now share `resolveModelTransport` (`open-sse/services/provider.js`), which prefers the sourceFormat-matched endpoint (no translation) and otherwise a model-declared one.
|
|
12
|
+
- OpenCode Go transports are ordered `claude → openai → openai-responses → gemini`, so the fallback prefers Anthropic Messages, which is what Claude Code traffic uses.
|
|
13
|
+
- OpenCode Go `supportedFormats` re-verified against the live endpoints (the published table is stale): `kimi-k3`, `kimi-k3.1-pro`, `minimax-m3` and `deepseek-v4.1-flash` also answer on `/messages`, most chat models also answer `/responses`, and `minimax-m2.7` is Messages-only. Models disabled in an account's console read as unavailable, so only `ModelProtocolUnsupported` narrows a model's formats.
|
|
14
|
+
- Added the documented `step-5-preview` and `beam-free` models, plus `gemini-4-argon` on the Google wire: the executor builds the model-scoped `/{model}:generateContent` URL and authenticates with `x-goog-api-key`.
|
|
15
|
+
|
|
5
16
|
## [0.1.16] — 2026-10-26
|
|
6
17
|
|
|
7
18
|
- Removed the fish-audio, glm-cn, groq, huggingface, minimax, minimax-cn, siliconflow, vercel-ai-gateway, xiaomi-tokenplan and xquik providers: registry entries, executors, usage, TTS and image handlers, logos and their tests are gone.
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// Limits and model access are enforced server-side; nothing here gates requests.
|
|
3
3
|
export const OPENCODE_GO_DOCS_URL = "https://opencode.ai/v2/docs/console/go";
|
|
4
4
|
|
|
5
|
-
// Wire formats
|
|
6
|
-
//
|
|
5
|
+
// Wire formats the connection probe covers, matching registry transport `format`
|
|
6
|
+
// values. The Google wire (`/google/v1beta/models/{model}`) is served for a single
|
|
7
|
+
// model and is not probed, because its URL depends on that model's availability.
|
|
7
8
|
export const OPENCODE_GO_ENDPOINTS = [
|
|
8
9
|
{
|
|
9
10
|
format: "openai",
|
|
@@ -50,6 +50,13 @@ export class OpenCodeGoExecutor extends DefaultExecutor {
|
|
|
50
50
|
buildUrl(model, stream, urlIndex = 0, credentials = null) {
|
|
51
51
|
// Muse Spark lives on /responses even when a stale runtimeTransport leaks in.
|
|
52
52
|
if (isResponsesModel(model)) return RESPONSES_BASE_URL;
|
|
53
|
+
// Google wire carries the model id in the path, unlike the OpenAI/Anthropic
|
|
54
|
+
// endpoints whose transport URL is the full target.
|
|
55
|
+
const transport = credentials?.runtimeTransport;
|
|
56
|
+
if (transport?.format === "gemini") {
|
|
57
|
+
const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
|
|
58
|
+
return `${transport.baseUrl}/${baseModelId(model)}:${action}`;
|
|
59
|
+
}
|
|
53
60
|
return super.buildUrl(model, stream, urlIndex, credentials);
|
|
54
61
|
}
|
|
55
62
|
|
|
@@ -6,6 +6,7 @@ import { getThinkingLevels } from "../providers/thinkingLevels.js";
|
|
|
6
6
|
import { injectReasoningContent } from "../utils/reasoningContentInjector.js";
|
|
7
7
|
import { resolveSessionId } from "../utils/sessionManager.js";
|
|
8
8
|
import { isMuseSparkModel } from "../providers/models/helpers.js";
|
|
9
|
+
import { opencodeTlsFetch, OpencodeTlsUnavailableError } from "../services/opencodeTlsClient.js";
|
|
9
10
|
import {
|
|
10
11
|
applyFingerprintTools,
|
|
11
12
|
detectToolShape,
|
|
@@ -357,6 +358,25 @@ export class OpenCodeExecutor extends BaseExecutor {
|
|
|
357
358
|
const result = await super.execute({
|
|
358
359
|
...args,
|
|
359
360
|
credentials: this.prepareRequestCredentials(args),
|
|
361
|
+
directFetch: async (url, options) => {
|
|
362
|
+
try {
|
|
363
|
+
return await opencodeTlsFetch(
|
|
364
|
+
url,
|
|
365
|
+
{
|
|
366
|
+
method: options.method,
|
|
367
|
+
headers: options.headers,
|
|
368
|
+
body: options.body,
|
|
369
|
+
},
|
|
370
|
+
{ signal: options.signal },
|
|
371
|
+
);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (error instanceof OpencodeTlsUnavailableError) {
|
|
374
|
+
// Fail-open: wreq-js not installed; use native fetch.
|
|
375
|
+
return fetch(url, options);
|
|
376
|
+
}
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
},
|
|
360
380
|
});
|
|
361
381
|
// Only a temporary quota window earns the breaker. Entitlement 429s
|
|
362
382
|
// ("free models work in OpenCode") are permanent plan gates: cooling the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { detectFormat, getTargetFormat,
|
|
1
|
+
import { detectFormat, getTargetFormat, resolveModelTransport } from "../services/provider.js";
|
|
2
2
|
import { translateRequest } from "../translator/index.js";
|
|
3
3
|
import {
|
|
4
4
|
applyThinking,
|
|
@@ -138,20 +138,13 @@ export async function handleChatCore({
|
|
|
138
138
|
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
|
|
139
139
|
const modelTargetFormat = getModelTargetFormat(alias, model);
|
|
140
140
|
// Multi-endpoint providers: pick transport matching sourceFormat → zero translation.
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
// /chat/completions
|
|
144
|
-
//
|
|
141
|
+
// Guarded per model: opencode-go models differ in endpoint support, so a
|
|
142
|
+
// claude-format request must not go to /messages for kimi/glm and an OpenAI one
|
|
143
|
+
// must not go to /chat/completions for claude-haiku-5-5 (upstream answers
|
|
144
|
+
// ModelProtocolUnsupported). When the client format has no endpoint, the model's
|
|
145
|
+
// preferred declared endpoint is used and translation handles the wire change.
|
|
145
146
|
const modelSupportedFormats = getModelSupportedFormats(alias, model);
|
|
146
|
-
const
|
|
147
|
-
// Per-model guard: when a model declares supportedFormats, only use the
|
|
148
|
-
// sourceFormat-matched transport if that format is declared (opencode-go models
|
|
149
|
-
// differ — kimi/glm only do /chat/completions). Undeclared models keep the
|
|
150
|
-
// upstream default (use the transport), preserving behavior for glm/deepseek/...
|
|
151
|
-
const useTransport =
|
|
152
|
-
!modelSupportedFormats || modelSupportedFormats.includes(sourceFormat)
|
|
153
|
-
? runtimeTransport
|
|
154
|
-
: null;
|
|
147
|
+
const useTransport = resolveModelTransport(provider, sourceFormat, modelSupportedFormats);
|
|
155
148
|
// A source-format-matched endpoint keeps the request lossless. Prefer it
|
|
156
149
|
// over a model-level targetFormat, which is only the fallback for clients
|
|
157
150
|
// whose wire format has no supported transport (for example MiniMax-M3:
|
|
@@ -253,6 +253,16 @@ export const MODEL_CAPABILITIES = {
|
|
|
253
253
|
contextWindow: 1000000,
|
|
254
254
|
maxOutput: 131072,
|
|
255
255
|
},
|
|
256
|
+
"glm-5.3-flash-free": {
|
|
257
|
+
vision: true,
|
|
258
|
+
videoInput: true,
|
|
259
|
+
pdf: true,
|
|
260
|
+
reasoning: true,
|
|
261
|
+
thinkingFormat: "openai",
|
|
262
|
+
thinkingCanDisable: false,
|
|
263
|
+
contextWindow: 1000000,
|
|
264
|
+
maxOutput: 128000,
|
|
265
|
+
},
|
|
256
266
|
"glm-4.6v": {
|
|
257
267
|
vision: true,
|
|
258
268
|
videoInput: true,
|
|
@@ -621,6 +631,26 @@ export const PROVIDER_CAPABILITIES = {
|
|
|
621
631
|
// /api/v2/image/upload and leaves image_urls/chat_context.imageUrls null
|
|
622
632
|
// (same as qodercli). reasoning:true on all of them — every model can
|
|
623
633
|
// reason; the upstream is_reasoning flag only drives model_config selection.
|
|
634
|
+
// OpenCode Free (oc)
|
|
635
|
+
opencode: {
|
|
636
|
+
"mimo-v2.6-flash-free": {
|
|
637
|
+
vision: true,
|
|
638
|
+
audioInput: true,
|
|
639
|
+
videoInput: true,
|
|
640
|
+
reasoning: true,
|
|
641
|
+
thinkingFormat: "openai",
|
|
642
|
+
thinkingCanDisable: true,
|
|
643
|
+
contextWindow: 1048576,
|
|
644
|
+
maxOutput: 131072,
|
|
645
|
+
},
|
|
646
|
+
"deepseek-v4.1-flash-free": {
|
|
647
|
+
vision: true,
|
|
648
|
+
reasoning: true,
|
|
649
|
+
thinkingFormat: "deepseek",
|
|
650
|
+
contextWindow: 330000,
|
|
651
|
+
maxOutput: 128000,
|
|
652
|
+
},
|
|
653
|
+
},
|
|
624
654
|
// thinkingFormat keeps the true-model family for documentation/UI, but
|
|
625
655
|
// thinkingCanDisable:false everywhere: the executor only forwards
|
|
626
656
|
// messages/tools/max_tokens, and thinking is fixed upstream via
|
|
@@ -25,3 +25,17 @@ export function isMuseSparkModel(modelId) {
|
|
|
25
25
|
const base = clean.includes("/") ? clean.split("/").pop() : clean;
|
|
26
26
|
return /^muse[-_]?spark(?:$|[-_:.\s])/i.test(base);
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
export function isDeepSeekModel(modelId) {
|
|
30
|
+
if (!modelId || typeof modelId !== "string") return false;
|
|
31
|
+
const clean = modelId.replace(/\([^()]+\)\s*$/, "").trim();
|
|
32
|
+
const base = clean.includes("/") ? clean.split("/").pop() : clean;
|
|
33
|
+
return /^deepseek-/i.test(base);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isOpencodeFreeTierModel(modelId) {
|
|
37
|
+
if (!modelId || typeof modelId !== "string") return false;
|
|
38
|
+
const clean = modelId.replace(/\([^()]+\)\s*$/, "").trim();
|
|
39
|
+
const base = clean.includes("/") ? clean.split("/").pop() : clean;
|
|
40
|
+
return /-free$/i.test(base);
|
|
41
|
+
}
|
|
@@ -25,8 +25,15 @@ export default {
|
|
|
25
25
|
},
|
|
26
26
|
// Multi-endpoint: pick the transport matching the client sourceFormat to skip
|
|
27
27
|
// translation. Guarded per-model by `supportedFormats` (see chatCore) because
|
|
28
|
-
// opencode-go models differ in endpoint support.
|
|
28
|
+
// opencode-go models differ in endpoint support. Order is the fallback
|
|
29
|
+
// preference when the client format has no endpoint on the model: Anthropic
|
|
30
|
+
// first (Claude Code is the main client), then Chat Completions, then Responses.
|
|
29
31
|
transports: [
|
|
32
|
+
{
|
|
33
|
+
format: "claude",
|
|
34
|
+
baseUrl: "https://opencode.ai/inference/go/anthropic/v1/messages",
|
|
35
|
+
auth: { combined: true, header: "x-api-key", scheme: "raw", anthropicVersion: true },
|
|
36
|
+
},
|
|
30
37
|
{
|
|
31
38
|
format: "openai",
|
|
32
39
|
baseUrl: "https://opencode.ai/inference/go/openai/v1/chat/completions",
|
|
@@ -35,58 +42,122 @@ export default {
|
|
|
35
42
|
// (400 unknown field "thinking"), so normalize reasoning to reasoning_effort.
|
|
36
43
|
thinkingFormat: "openai",
|
|
37
44
|
},
|
|
38
|
-
{
|
|
39
|
-
format: "claude",
|
|
40
|
-
baseUrl: "https://opencode.ai/inference/go/anthropic/v1/messages",
|
|
41
|
-
auth: { combined: true, header: "x-api-key", scheme: "raw", anthropicVersion: true },
|
|
42
|
-
},
|
|
43
45
|
{
|
|
44
46
|
format: "openai-responses",
|
|
45
47
|
baseUrl: "https://opencode.ai/inference/go/openai/v1/responses",
|
|
46
48
|
auth: { combined: true, header: "Authorization", scheme: "bearer" },
|
|
47
49
|
thinkingFormat: "openai",
|
|
48
50
|
},
|
|
51
|
+
{
|
|
52
|
+
// Google protocol: the model id is a path segment, so the executor appends
|
|
53
|
+
// `/{model}:generateContent` instead of using baseUrl as the full endpoint.
|
|
54
|
+
format: "gemini",
|
|
55
|
+
baseUrl: "https://opencode.ai/inference/go/google/v1beta/models",
|
|
56
|
+
auth: { combined: true, header: "x-goog-api-key", scheme: "raw" },
|
|
57
|
+
},
|
|
49
58
|
],
|
|
50
|
-
// supportedFormats
|
|
59
|
+
// supportedFormats are the endpoints each model actually answers on. The docs table
|
|
60
|
+
// is incomplete (it omits models a Go key can reach and lists protocols the gateway
|
|
61
|
+
// rejects); these values were verified against the live Go endpoints. claude comes
|
|
62
|
+
// first on purpose: most 10Router traffic is Claude Code, which then needs no
|
|
63
|
+
// translation. Probes ran on one account, so "model unavailable" can mean the model
|
|
64
|
+
// is disabled in that account's console — do not narrow a model's formats for it;
|
|
65
|
+
// only ModelProtocolUnsupported proves an endpoint is unsupported.
|
|
51
66
|
models: [
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
{
|
|
68
|
+
id: "deepseek-v4.1-flash",
|
|
69
|
+
name: "DeepSeek V4.1 Flash",
|
|
70
|
+
supportedFormats: ["claude", "openai", "openai-responses"],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "glm-5.3-flash",
|
|
74
|
+
name: "GLM 5.3 Flash (Vision)",
|
|
75
|
+
supportedFormats: ["openai", "openai-responses"],
|
|
76
|
+
},
|
|
77
|
+
{ id: "glm-5.3", name: "GLM 5.3", supportedFormats: ["openai", "openai-responses"] },
|
|
78
|
+
{ id: "glm-5.2", name: "GLM 5.2", supportedFormats: ["openai", "openai-responses"] },
|
|
79
|
+
{ id: "glm-5.1", name: "GLM 5.1", supportedFormats: ["openai", "openai-responses"] },
|
|
80
|
+
{
|
|
81
|
+
id: "kimi-k2.7-code",
|
|
82
|
+
name: "Kimi K2.7 Code",
|
|
83
|
+
supportedFormats: ["openai", "openai-responses"],
|
|
84
|
+
},
|
|
85
|
+
{ id: "kimi-k2.6", name: "Kimi K2.6", supportedFormats: ["openai", "openai-responses"] },
|
|
86
|
+
{
|
|
87
|
+
id: "kimi-k3",
|
|
88
|
+
name: "Kimi K3",
|
|
89
|
+
supportedFormats: ["claude", "openai", "openai-responses"],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "kimi-k3.1-pro",
|
|
93
|
+
name: "Kimi K3.1 Pro",
|
|
94
|
+
supportedFormats: ["claude", "openai", "openai-responses"],
|
|
95
|
+
},
|
|
61
96
|
{
|
|
62
97
|
id: "deepseek-v4-pro",
|
|
63
98
|
name: "DeepSeek V4 Pro",
|
|
64
|
-
supportedFormats: ["
|
|
99
|
+
supportedFormats: ["claude", "openai", "openai-responses"],
|
|
65
100
|
},
|
|
66
101
|
{
|
|
67
102
|
id: "deepseek-v4-flash",
|
|
68
103
|
name: "DeepSeek V4 Flash",
|
|
69
|
-
supportedFormats: ["
|
|
104
|
+
supportedFormats: ["claude", "openai", "openai-responses"],
|
|
70
105
|
},
|
|
71
106
|
{
|
|
72
107
|
id: "deepseek-v4-flash-vision-exp",
|
|
73
108
|
name: "DeepSeek V4 Flash Vision (Exp)",
|
|
74
|
-
supportedFormats: ["
|
|
109
|
+
supportedFormats: ["claude", "openai", "openai-responses"],
|
|
110
|
+
},
|
|
111
|
+
{ id: "longcat-2.0", name: "LongCat 2.0", supportedFormats: ["openai", "openai-responses"] },
|
|
112
|
+
{
|
|
113
|
+
id: "mimo-v2.6-flash",
|
|
114
|
+
name: "MiMo V2.6 Flash",
|
|
115
|
+
supportedFormats: ["openai", "openai-responses"],
|
|
75
116
|
},
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
117
|
+
{
|
|
118
|
+
id: "mimo-v2.6-pro",
|
|
119
|
+
name: "MiMo V2.6 Pro",
|
|
120
|
+
supportedFormats: ["openai", "openai-responses"],
|
|
121
|
+
},
|
|
122
|
+
{ id: "mimo-v2.5", name: "MiMo V2.5", supportedFormats: ["openai", "openai-responses"] },
|
|
123
|
+
// "Unsupported model" on every wire: either retired or disabled in the account's
|
|
124
|
+
// console, which the probe cannot tell apart. Keep the documented Chat Completions
|
|
125
|
+
// route instead of narrowing what other accounts can use.
|
|
80
126
|
{ id: "mimo-v2.5-pro", name: "MiMo V2.5 Pro", supportedFormats: ["openai"] },
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
127
|
+
{
|
|
128
|
+
id: "minimax-m3",
|
|
129
|
+
name: "MiniMax M3",
|
|
130
|
+
supportedFormats: ["claude", "openai", "openai-responses"],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: "minimax-m3.1-flash",
|
|
134
|
+
name: "MiniMax M3.1 Flash",
|
|
135
|
+
supportedFormats: ["claude", "openai"],
|
|
136
|
+
},
|
|
137
|
+
{ id: "minimax-m2.7", name: "MiniMax M2.7", supportedFormats: ["claude"] },
|
|
84
138
|
{ id: "claude-haiku-5-5", name: "Claude Haiku 5.5", supportedFormats: ["claude"] },
|
|
85
|
-
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
139
|
+
{
|
|
140
|
+
id: "qwen3.8-max",
|
|
141
|
+
name: "Qwen 3.8 Max",
|
|
142
|
+
supportedFormats: ["claude", "openai", "openai-responses"],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "qwen3.8-flash",
|
|
146
|
+
name: "Qwen 3.8 Flash",
|
|
147
|
+
supportedFormats: ["claude", "openai", "openai-responses"],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "qwen3.7-plus",
|
|
151
|
+
name: "Qwen 3.7 Plus",
|
|
152
|
+
supportedFormats: ["claude", "openai", "openai-responses"],
|
|
153
|
+
},
|
|
154
|
+
{ id: "hy4-preview", name: "Hy4 Preview", supportedFormats: ["openai", "openai-responses"] },
|
|
155
|
+
{ id: "hy3", name: "Hy3", supportedFormats: ["openai", "openai-responses"] },
|
|
156
|
+
{
|
|
157
|
+
id: "step-5-preview",
|
|
158
|
+
name: "Step 5 Preview",
|
|
159
|
+
supportedFormats: ["openai", "openai-responses"],
|
|
160
|
+
},
|
|
90
161
|
{
|
|
91
162
|
id: "space-bunny-free",
|
|
92
163
|
name: "Space Bunny Free",
|
|
@@ -99,6 +170,20 @@ export default {
|
|
|
99
170
|
supportedFormats: ["openai"],
|
|
100
171
|
tier: "free",
|
|
101
172
|
},
|
|
173
|
+
{
|
|
174
|
+
id: "beam-free",
|
|
175
|
+
name: "Beam Free",
|
|
176
|
+
supportedFormats: ["openai", "openai-responses"],
|
|
177
|
+
tier: "free",
|
|
178
|
+
},
|
|
179
|
+
// Google wire only (documented). Every client format pivots through OpenAI to
|
|
180
|
+
// gemini, and the executor builds `/{model}:generateContent` from the transport.
|
|
181
|
+
{
|
|
182
|
+
id: "gemini-4-argon",
|
|
183
|
+
name: "Gemini 4 Argon",
|
|
184
|
+
targetFormat: "gemini",
|
|
185
|
+
supportedFormats: ["gemini"],
|
|
186
|
+
},
|
|
102
187
|
// Served by /inference/go/openai/v1/responses only — the responses-only entry forces chatCore
|
|
103
188
|
// past the sourceFormat-matched transports into translation (see chatCore guard).
|
|
104
189
|
{
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Lazy loader for heavy optional runtime deps (playwright-core, wreq-js).
|
|
2
|
+
//
|
|
3
|
+
// They are devDependencies only, so end-user installs stay small. On first
|
|
4
|
+
// use the package is downloaded into <DATA_DIR>/runtime and imported from there.
|
|
5
|
+
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { getWritableDataDir } from "../utils/writableDataDir.js";
|
|
11
|
+
|
|
12
|
+
const LAZY_DEP_VERSIONS = {
|
|
13
|
+
"playwright-core": "^1.62.1",
|
|
14
|
+
"wreq-js": "^3.2.0",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function getRuntimeDir() {
|
|
18
|
+
return path.join(getWritableDataDir(), "runtime");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let createRequirePromise;
|
|
22
|
+
function getCreateRequire() {
|
|
23
|
+
createRequirePromise ??= import(/* webpackIgnore: true */ "node:module").then(
|
|
24
|
+
(mod) => mod.createRequire,
|
|
25
|
+
);
|
|
26
|
+
return createRequirePromise;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isModuleMissing(error) {
|
|
30
|
+
const code = String(error?.code || "");
|
|
31
|
+
return code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function npmInstall(pkg, version) {
|
|
35
|
+
const cwd = getRuntimeDir();
|
|
36
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
37
|
+
const manifest = path.join(cwd, "package.json");
|
|
38
|
+
if (!fs.existsSync(manifest)) {
|
|
39
|
+
fs.writeFileSync(
|
|
40
|
+
manifest,
|
|
41
|
+
JSON.stringify({ name: "10router-runtime", private: true }, null, 2),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
45
|
+
const args = ["install", "--no-audit", "--no-fund", "--prefer-online", `${pkg}@${version}`];
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const child = spawn(npm, args, {
|
|
48
|
+
cwd,
|
|
49
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
50
|
+
shell: process.platform === "win32",
|
|
51
|
+
});
|
|
52
|
+
let stderr = "";
|
|
53
|
+
child.stderr?.on("data", (chunk) => {
|
|
54
|
+
stderr += String(chunk);
|
|
55
|
+
});
|
|
56
|
+
child.on("error", (error) => resolve({ ok: false, stderr: String(error.message || error) }));
|
|
57
|
+
child.on("close", (status) => resolve({ ok: status === 0, stderr }));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function installIntoRuntime(pkg, version) {
|
|
62
|
+
const res = await npmInstall(pkg, version);
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
const reason =
|
|
65
|
+
(res.stderr.match(/npm ERR! ?(.+)/) || [])[1] ||
|
|
66
|
+
res.stderr.split("\n").filter(Boolean).pop() ||
|
|
67
|
+
"unknown npm error";
|
|
68
|
+
throw new Error(
|
|
69
|
+
`${pkg} is missing and the on-demand install failed (${String(reason).slice(0, 160)}) — ` +
|
|
70
|
+
`install it manually: cd "${getRuntimeDir()}" && npm install ${pkg}@${version}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const inflight = new Map();
|
|
76
|
+
|
|
77
|
+
async function load(pkg) {
|
|
78
|
+
try {
|
|
79
|
+
return await import(/* webpackIgnore: true */ pkg);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (!isModuleMissing(error)) throw error;
|
|
82
|
+
}
|
|
83
|
+
const createRequire = await getCreateRequire();
|
|
84
|
+
const runtimeRequire = createRequire(path.join(getRuntimeDir(), "package.json"));
|
|
85
|
+
try {
|
|
86
|
+
return await import(/* webpackIgnore: true */ pathToFileURL(runtimeRequire.resolve(pkg)).href);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!isModuleMissing(error)) throw error;
|
|
89
|
+
}
|
|
90
|
+
const version = LAZY_DEP_VERSIONS[pkg] || "latest";
|
|
91
|
+
await installIntoRuntime(pkg, version);
|
|
92
|
+
try {
|
|
93
|
+
return await import(/* webpackIgnore: true */ pathToFileURL(runtimeRequire.resolve(pkg)).href);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (!isModuleMissing(error)) throw error;
|
|
96
|
+
fs.rmSync(path.join(getRuntimeDir(), "node_modules", pkg), { recursive: true, force: true });
|
|
97
|
+
await installIntoRuntime(pkg, version);
|
|
98
|
+
return import(/* webpackIgnore: true */ pathToFileURL(runtimeRequire.resolve(pkg)).href);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function loadRuntimeDep(pkg) {
|
|
103
|
+
if (!inflight.has(pkg)) {
|
|
104
|
+
inflight.set(
|
|
105
|
+
pkg,
|
|
106
|
+
load(pkg).finally(() => inflight.delete(pkg)),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return inflight.get(pkg);
|
|
110
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// TLS-impersonating HTTP client for opencode.ai's free tier.
|
|
2
|
+
|
|
3
|
+
import { loadRuntimeDep } from "./lazyRuntime.js";
|
|
4
|
+
|
|
5
|
+
const OPENCODE_CIPHER_LIST = [
|
|
6
|
+
"TLS_AES_128_GCM_SHA256",
|
|
7
|
+
"TLS_AES_256_GCM_SHA384",
|
|
8
|
+
"TLS_CHACHA20_POLY1305_SHA256",
|
|
9
|
+
"ECDHE-ECDSA-AES128-GCM-SHA256",
|
|
10
|
+
"ECDHE-RSA-AES128-GCM-SHA256",
|
|
11
|
+
"ECDHE-ECDSA-AES256-GCM-SHA384",
|
|
12
|
+
"ECDHE-RSA-AES256-GCM-SHA384",
|
|
13
|
+
"ECDHE-ECDSA-CHACHA20-POLY1305",
|
|
14
|
+
"ECDHE-RSA-CHACHA20-POLY1305",
|
|
15
|
+
"ECDHE-ECDSA-AES128-SHA",
|
|
16
|
+
"ECDHE-RSA-AES128-SHA",
|
|
17
|
+
"ECDHE-ECDSA-AES256-SHA",
|
|
18
|
+
"ECDHE-RSA-AES256-SHA",
|
|
19
|
+
"AES128-GCM-SHA256",
|
|
20
|
+
"AES256-GCM-SHA384",
|
|
21
|
+
"AES128-SHA",
|
|
22
|
+
"AES256-SHA",
|
|
23
|
+
].join(":");
|
|
24
|
+
|
|
25
|
+
const DEFAULT_TIMEOUT_MS = 120000;
|
|
26
|
+
|
|
27
|
+
let sessionPromise = null;
|
|
28
|
+
|
|
29
|
+
export class OpencodeTlsUnavailableError extends Error {
|
|
30
|
+
constructor(message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "OpencodeTlsUnavailableError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function loadSession() {
|
|
37
|
+
if (!sessionPromise) {
|
|
38
|
+
sessionPromise = (async () => {
|
|
39
|
+
const wreq = await loadRuntimeDep("wreq-js");
|
|
40
|
+
if (typeof wreq.createSession !== "function") {
|
|
41
|
+
throw new Error("wreq-js runtime is missing createSession");
|
|
42
|
+
}
|
|
43
|
+
return wreq.createSession({
|
|
44
|
+
emulation: { tlsOptions: { cipherList: OPENCODE_CIPHER_LIST } },
|
|
45
|
+
});
|
|
46
|
+
})().catch((error) => {
|
|
47
|
+
sessionPromise = null;
|
|
48
|
+
throw error;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return sessionPromise;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resetOpencodeTlsSession() {
|
|
55
|
+
const pending = sessionPromise;
|
|
56
|
+
sessionPromise = null;
|
|
57
|
+
pending?.then((session) => session.close?.()).catch(() => {});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function opencodeTlsFetch(
|
|
61
|
+
url,
|
|
62
|
+
init = {},
|
|
63
|
+
{ signal, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
64
|
+
) {
|
|
65
|
+
let session;
|
|
66
|
+
try {
|
|
67
|
+
session = await loadSession();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw new OpencodeTlsUnavailableError(
|
|
70
|
+
`wreq-js unavailable (${error instanceof Error ? error.message : "unknown"}) — install the optional wreq-js dependency to reach the OpenCode free tier`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const options = {
|
|
75
|
+
method: init.method || "GET",
|
|
76
|
+
headers: init.headers,
|
|
77
|
+
redirect: "follow",
|
|
78
|
+
timeout: timeoutMs,
|
|
79
|
+
};
|
|
80
|
+
if (init.body !== undefined) options.body = init.body;
|
|
81
|
+
if (signal) options.signal = signal;
|
|
82
|
+
|
|
83
|
+
const res = await session.fetch(url, options);
|
|
84
|
+
const headers = {};
|
|
85
|
+
res.headers?.forEach?.((value, key) => {
|
|
86
|
+
headers[key] = value;
|
|
87
|
+
});
|
|
88
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const __test__ = { OPENCODE_CIPHER_LIST };
|
|
@@ -158,6 +158,33 @@ export function resolveTransport(provider, sourceFormat) {
|
|
|
158
158
|
return transports.find((t) => t.format === sourceFormat) || null;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
// Fallback transport for a multi-endpoint provider when the client's wire format
|
|
162
|
+
// has no endpoint on this model (per-model `supportedFormats`). Without it the
|
|
163
|
+
// request would go to the provider's default URL and upstream would answer
|
|
164
|
+
// ModelProtocolUnsupported (opencode-go: claude-haiku-5-5 has no /chat/completions).
|
|
165
|
+
// The registry's transport order is the preference order, so providers can put the
|
|
166
|
+
// endpoint real clients use most first.
|
|
167
|
+
export function resolveFallbackTransport(provider, supportedFormats) {
|
|
168
|
+
if (!Array.isArray(supportedFormats) || !supportedFormats.length) return null;
|
|
169
|
+
const config = PROVIDERS[provider];
|
|
170
|
+
const transports = config?.transports;
|
|
171
|
+
if (!Array.isArray(transports) || !transports.length) return null;
|
|
172
|
+
return transports.find((t) => supportedFormats.includes(t.format || "openai")) || null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Transport for one model: the sourceFormat-matched endpoint when the model
|
|
176
|
+
// declares that format (zero translation), otherwise the model's preferred
|
|
177
|
+
// declared endpoint. Undeclared models keep the old behavior — the matched
|
|
178
|
+
// transport or the provider default.
|
|
179
|
+
export function resolveModelTransport(provider, sourceFormat, supportedFormats) {
|
|
180
|
+
const matchesSource = !supportedFormats || supportedFormats.includes(sourceFormat);
|
|
181
|
+
if (matchesSource) {
|
|
182
|
+
const matched = resolveTransport(provider, sourceFormat);
|
|
183
|
+
if (matched) return matched;
|
|
184
|
+
}
|
|
185
|
+
return resolveFallbackTransport(provider, supportedFormats);
|
|
186
|
+
}
|
|
187
|
+
|
|
161
188
|
// Check if last message is from user
|
|
162
189
|
export function isLastMessageFromUser(body) {
|
|
163
190
|
const messages = body.messages || body.contents;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Resolve a writable data directory: DATA_DIR first, then ~/.10router, then
|
|
2
|
+
// legacy ~/.9router, then tmpdir. Some installs point DATA_DIR at root-owned
|
|
3
|
+
// paths that the server process cannot write browser profiles or runtime
|
|
4
|
+
// installs into.
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
let cached = null;
|
|
11
|
+
|
|
12
|
+
export function getWritableDataDir() {
|
|
13
|
+
if (cached) return cached;
|
|
14
|
+
const candidates = [
|
|
15
|
+
process.env.DATA_DIR,
|
|
16
|
+
path.join(os.homedir(), ".10router"),
|
|
17
|
+
path.join(os.homedir(), ".9router"),
|
|
18
|
+
path.join(os.tmpdir(), "10router"),
|
|
19
|
+
].filter(Boolean);
|
|
20
|
+
for (const base of candidates) {
|
|
21
|
+
try {
|
|
22
|
+
fs.mkdirSync(base, { recursive: true });
|
|
23
|
+
fs.accessSync(base, fs.constants.W_OK);
|
|
24
|
+
cached = base;
|
|
25
|
+
return base;
|
|
26
|
+
} catch {
|
|
27
|
+
// Not writable — try the next candidate.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
cached = process.cwd();
|
|
31
|
+
return cached;
|
|
32
|
+
}
|