feat: add orbit provider

This commit is contained in:
2026-07-17 16:17:50 +07:00
parent 0b7bd09646
commit 7ff4ebeed1
8 changed files with 194 additions and 1 deletions
+1
View File
@@ -1,6 +1,7 @@
# v0.5.35 (2026-07-16) # v0.5.35 (2026-07-16)
## Features ## Features
- **Orbit Provider**: add Anthropic-compatible API-key routing for Claude Opus 4.64.8 models
- **xAI**: Grok Imagine video generation (`/v1/videos`) + CLI - **xAI**: Grok Imagine video generation (`/v1/videos`) + CLI
- **CLI tools**: Grok Build setup — writes `[model.9router]` to `~/.grok/config.toml` - **CLI tools**: Grok Build setup — writes `[model.9router]` to `~/.grok/config.toml`
- **GitHub Copilot**: route Claude models through Copilot's native `/v1/messages` - **GitHub Copilot**: route Claude models through Copilot's native `/v1/messages`
+3 -1
View File
@@ -99,6 +99,7 @@ import p96 from "./xai.js";
import p97 from "./xiaomi-mimo.js"; import p97 from "./xiaomi-mimo.js";
import p98 from "./xiaomi-tokenplan.js"; import p98 from "./xiaomi-tokenplan.js";
import p99 from "./youcom.js"; import p99 from "./youcom.js";
import p100 from "./orbit-provider.js";
export default [ export default [
p0, p0,
@@ -200,5 +201,6 @@ export default [
p96, p96,
p97, p97,
p98, p98,
p99 p99,
p100
]; ];
@@ -0,0 +1,31 @@
export default {
id: "orbit-provider",
priority: 100,
alias: "orbit",
display: {
name: "Orbit Provider",
icon: "public_dns",
color: "#8B5CF6",
textIcon: "OB",
},
category: "apikey",
authType: "apikey",
transport: {
baseUrl: "https://api.orbit-provider.com/anthropic/v1/messages",
format: "claude",
headers: {
"anthropic-version": "2023-06-01",
},
},
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-opus-4-7", name: "Claude Opus 4.7" },
{ id: "claude-opus-4-6", name: "Claude Opus 4.6" },
{ id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 (Thinking)" },
],
serviceKinds: ["llm"],
thinkingConfig: {
options: ["auto", "on", "off"],
defaultMode: "auto",
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -19,6 +19,7 @@ import {
KIMCHI_CONFIG, KIMCHI_CONFIG,
} from "@/lib/oauth/constants/oauth"; } from "@/lib/oauth/constants/oauth";
import { buildClineHeaders } from "@/shared/utils/clineAuth"; import { buildClineHeaders } from "@/shared/utils/clineAuth";
import { validateConfiguredClaudeApiKey } from "@/lib/providers/apiKeyValidation";
// OAuth provider test endpoints // OAuth provider test endpoints
const OAUTH_TEST_CONFIG = { const OAUTH_TEST_CONFIG = {
@@ -526,6 +527,13 @@ async function testApiKeyConnection(connection, effectiveProxy = null) {
} }
try { try {
const configuredClaudeResult = await validateConfiguredClaudeApiKey(
connection.provider,
connection.apiKey,
(url, options) => fetchWithConnectionProxy(url, options, effectiveProxy),
);
if (configuredClaudeResult !== null) return configuredClaudeResult;
switch (connection.provider) { switch (connection.provider) {
case "cloudflare-ai": { case "cloudflare-ai": {
const psd = connection.providerSpecificData || {}; const psd = connection.providerSpecificData || {};
+8
View File
@@ -6,6 +6,7 @@ import { resolveOllamaLocalHost, resolveXiaomiTokenplanBaseUrl, PROVIDERS } from
import { openaiToCommandCodeRequest } from "open-sse/translator/request/openai-to-commandcode.js"; import { openaiToCommandCodeRequest } from "open-sse/translator/request/openai-to-commandcode.js";
import { normalizeProviderId } from "@/lib/providerNormalization"; import { normalizeProviderId } from "@/lib/providerNormalization";
import { requireProviderAdministrator } from "@/lib/providers/connectionAccess"; import { requireProviderAdministrator } from "@/lib/providers/connectionAccess";
import { validateConfiguredClaudeApiKey } from "@/lib/providers/apiKeyValidation";
// Probe a webSearch/webFetch provider using its searchConfig/fetchConfig. // Probe a webSearch/webFetch provider using its searchConfig/fetchConfig.
// Returns true if API key is accepted (status !== 401 && !== 403). // Returns true if API key is accepted (status !== 401 && !== 403).
@@ -253,6 +254,13 @@ export async function POST(request) {
}); });
} }
// Registry-backed Anthropic-compatible providers share one config-driven
// validation flow instead of requiring a hardcoded switch case.
const claudeResult = await validateConfiguredClaudeApiKey(provider, apiKey);
if (claudeResult !== null) {
return NextResponse.json(claudeResult);
}
switch (provider) { switch (provider) {
case "openai": case "openai":
const openaiRes = await fetch("https://api.openai.com/v1/models", { const openaiRes = await fetch("https://api.openai.com/v1/models", {
+56
View File
@@ -0,0 +1,56 @@
import { getModelsByProviderId } from "open-sse/config/providerModels.js";
import { PROVIDERS } from "open-sse/config/providers.js";
function setAuthHeader(headers, auth, apiKey) {
if (!auth?.header) return false;
headers[auth.header] = auth.scheme === "bearer" ? `Bearer ${apiKey}` : apiKey;
return true;
}
function buildClaudeValidationHeaders(config, apiKey) {
const headers = {
"Content-Type": "application/json",
...(config.headers || {}),
};
const auth = config.auth;
if (auth?.combined) {
setAuthHeader(headers, auth, apiKey);
} else if (auth?.apiKey) {
setAuthHeader(headers, auth.apiKey, apiKey);
} else if (!setAuthHeader(headers, auth, apiKey)) {
headers["x-api-key"] = apiKey;
}
return headers;
}
/**
* Validate an API key for a registry-backed Claude-format provider.
* A non-auth error still confirms that the upstream accepted the credential.
*
* @returns {Promise<{valid: boolean, error: string|null}|null>} null when the
* provider is not backed by a Claude transport.
*/
export async function validateConfiguredClaudeApiKey(provider, apiKey, fetchImpl = fetch) {
const config = PROVIDERS[provider];
if (!config?.baseUrl || config.format !== "claude") return null;
const model = getModelsByProviderId(provider)?.[0]?.id;
const response = await fetchImpl(config.baseUrl, {
method: "POST",
headers: buildClaudeValidationHeaders(config, apiKey),
body: JSON.stringify({
model,
max_tokens: 1,
messages: [{ role: "user", content: "test" }],
}),
signal: AbortSignal.timeout(10000),
});
const valid = response.status !== 401 && response.status !== 403;
return {
valid,
error: valid ? null : "Invalid API key",
};
}
+87
View File
@@ -0,0 +1,87 @@
import { describe, expect, it, vi } from "vitest";
import { getExecutor } from "../../open-sse/executors/index.js";
import { PROVIDERS, PROVIDER_MODELS } from "../../open-sse/providers/index.js";
import REGISTRY from "../../open-sse/providers/registry/index.js";
import { validateConfiguredClaudeApiKey } from "../../src/lib/providers/apiKeyValidation.js";
describe("Orbit Provider", () => {
const orbit = REGISTRY.find((entry) => entry.id === "orbit-provider");
it("registers an Anthropic-compatible API-key provider", () => {
expect(orbit).toBeDefined();
expect(orbit).toMatchObject({
alias: "orbit",
category: "apikey",
authType: "apikey",
serviceKinds: ["llm"],
transport: {
baseUrl: "https://api.orbit-provider.com/anthropic/v1/messages",
format: "claude",
},
});
});
it("builds the Claude transport with default x-api-key authentication", () => {
expect(PROVIDERS["orbit-provider"]).toMatchObject({
baseUrl: "https://api.orbit-provider.com/anthropic/v1/messages",
format: "claude",
headers: {
"anthropic-version": "2023-06-01",
},
});
const executor = getExecutor("orbit-provider");
expect(executor.buildUrl("claude-opus-4-8", true)).toBe(
"https://api.orbit-provider.com/anthropic/v1/messages",
);
expect(executor.buildHeaders({ apiKey: "orbit-test-key" })).toMatchObject({
"Content-Type": "application/json",
"anthropic-version": "2023-06-01",
"x-api-key": "orbit-test-key",
});
});
it("exposes the configured Opus models and thinking controls", () => {
expect((PROVIDER_MODELS.orbit || []).map((model) => model.id)).toEqual([
"claude-opus-4-8",
"claude-opus-4-7",
"claude-opus-4-6",
"claude-opus-4-6-thinking",
]);
expect(orbit.thinkingConfig).toEqual({
options: ["auto", "on", "off"],
defaultMode: "auto",
});
});
it("validates its API key against the configured Claude endpoint", async () => {
const fetchMock = vi.fn().mockResolvedValue({ status: 400 });
await expect(
validateConfiguredClaudeApiKey("orbit-provider", "orbit-test-key", fetchMock),
).resolves.toEqual({ valid: true, error: null });
expect(fetchMock).toHaveBeenCalledWith(
"https://api.orbit-provider.com/anthropic/v1/messages",
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
"x-api-key": "orbit-test-key",
"anthropic-version": "2023-06-01",
}),
}),
);
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toMatchObject({
model: "claude-opus-4-8",
max_tokens: 1,
});
});
it.each([401, 403])("rejects authentication status %i", async (status) => {
const fetchMock = vi.fn().mockResolvedValue({ status });
await expect(
validateConfiguredClaudeApiKey("orbit-provider", "bad-key", fetchMock),
).resolves.toEqual({ valid: false, error: "Invalid API key" });
});
});