diff --git a/scripts/test-layout/layout-providers.json b/scripts/test-layout/layout-providers.json new file mode 100644 index 00000000000..318ce2a79d0 --- /dev/null +++ b/scripts/test-layout/layout-providers.json @@ -0,0 +1,3 @@ +{ + "free-directory-runcrate.test.ts": "providers" +} diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index ba88b63756d..c0ec0cbd94f 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1,7 +1,7 @@ { "version": 1, "root": "tests", - "explicitShards": ["layout-clients.json", "layout-codex-collision.json", "layout-agent-assertion.json", "layout-chat.json", "layout-replay.json", "layout-adapters.json", "layout-jev.json", "layout-workbuddy.json", "layout-combos.json", "layout-credit-renewal.json"], + "explicitShards": ["layout-clients.json", "layout-codex-collision.json", "layout-agent-assertion.json", "layout-chat.json", "layout-replay.json", "layout-adapters.json", "layout-jev.json", "layout-workbuddy.json", "layout-combos.json", "layout-credit-renewal.json", "layout-providers.json"], "explicit": { "provider-input-modalities.test.ts": "providers", "combo-description.test.ts": "routing", "combo-description-catalog.test.ts": "codex-integration", "codex-provider-routing.test.ts": "codex-integration", "config-native-reasoning-retention.test.ts": "config", "server-native-reasoning-retention-merge.test.ts": "server", "server-native-reasoning-retention.test.ts": "server", "responses-retained-reasoning-stream-recovery.test.ts": "responses", "openai-native-hosted-search-history.test.ts": "adapters/openai", "responses-native-compact-search-history.test.ts": "responses", "config-hosted-search-projection.test.ts": "config", "gcp-credential-boundary.test.ts": "adapters/google", "gcp-sa-marker.test.ts": "adapters/google", "gcp-sa-adapter.test.ts": "adapters/google", "management-gcp-credential-input.test.ts": "server", "codex-preserve-signed-in.test.ts": "codex-integration", "cli-account-preservation.test.ts": "cli", "cli-provider-list-active.test.ts": "cli", "service-state-isolation.test.ts": "service", "compaction-history-framing.test.ts": "responses", "catalog-native-max-context.test.ts": "codex-integration", "protocol-argon-thought-boundary.test.ts": "responses", "service-log.test.ts": "service", "messages-native-empty-inline.test.ts": "claude-integration", "catalog-auto-review-override.test.ts": "codex-integration", "management-auto-review-settings.test.ts": "server", "management-auto-review-auth.test.ts": "server", "prefer-model-alias-ids.test.ts": "server", "responses-mimo-envelope.test.ts": "responses", "responses-call-id-linkage.test.ts": "responses", "bridge-terminal-refusal.test.ts": "adapters", "loop-terminal-refusal.test.ts": "images", "web-search-terminal-refusal.test.ts": "web-search", "combo-terminal-refusal.test.ts": "routing", "server-terminal-refusal-wiring.test.ts": "server", "incomplete-reason-normalization.test.ts": "responses", "advisor-retention.test.ts": "advisor", "responses-native-key-cooldown.test.ts": "responses", "responses-run-turn-key-readmission.test.ts": "responses", "qoder-spawn-readmission.test.ts": "adapters", "api-key-selection-cooldown.test.ts": "providers", "api-key-auto-pause-distribution.test.ts": "providers", "explicit-key-pause-oauth-boundary.test.ts": "providers", "server-chat-native-key-selection.test.ts": "server", "compact-key-selection.test.ts": "responses", "messages-native-key-selection.test.ts": "claude-integration", "coding-agent-windows-dispatch.test.ts": "adapters", "coding-agent-native-empty-argv.test.ts": "windows", "service-proxy-env.test.ts": "service", "openai-chat-standalone-bare-block.test.ts": "adapters/openai", "owner-registry-isolation.test.ts": "config", diff --git a/src/providers/free-directory.ts b/src/providers/free-directory.ts index 467c3e6b7d7..9653fe27694 100644 --- a/src/providers/free-directory.ts +++ b/src/providers/free-directory.ts @@ -12,7 +12,7 @@ export const FREE_PROVIDER_ACCESS_GROUPS = { "cloudflare-ai", "cohere", "coze", "duckduckgo-web", "felo-web", "friendliai", "gemini", "github-models", "groq", "hackclub", "huggingchat", "huggingface", "iflytek", "inference-net", "kiro", "liquid", "llm7", "mistral", "morph", "muse-spark-web", "nara", "navy", "nlpcloud", "ollama-cloud", "opencode", "openrouter", - "ovhcloud", "pollinations", "puter", "qwen-web", "reka", "sambanova", "sparkdesk", "t3-web", "uncloseai", + "ovhcloud", "pollinations", "puter", "qwen-web", "reka", "runcrate", "sambanova", "sparkdesk", "t3-web", "uncloseai", ], "recurring-uncapped": [ "agnes", "ainative", "aion", "baidu", "glm", "glm-cn", "kilo-gateway", "opencode-zen", "requesty", @@ -119,6 +119,14 @@ const CONNECTABLE: Record = { ovhcloud: openAi("https://oai.endpoints.kepler.ai.cloud.ovh.net/v1", "https://console.ovhcloud.com", { keyOptional: true, modelsUrl: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/models" }), pollinations: openAi("https://gen.pollinations.ai/v1", "https://pollinations.ai", { keyOptional: true }), reka: openAi("https://api.reka.ai/v1", "https://platform.reka.ai"), + // Checked 2026-10-27. Free terms: https://www.runcrate.ai/inference/free covers six named open + // models only, one allowance per GitHub account at least 30 days old, limited per session and per + // week and shared by the workspace; a workspace with credit continues as paid traffic once the + // allowance is used or free capacity is busy. Endpoint: https://www.runcrate.ai/docs/models/chat-completions. + // Operator: Aeonmind LLC (https://www.runcrate.ai/terms). GET /v1/models is public and returned + // 193 rows (text, image, video, audio, embeddings), all with paid list prices; six carry free_tier: true. + // Discovery is hybrid: the six free ids are seeded, and live discovery still lists the full paid catalog. + runcrate: openAi("https://api.runcrate.ai/v1", "https://www.runcrate.ai/dashboard/api-keys", { verification: "official", documentationUrl: "https://www.runcrate.ai/docs/models/chat-completions", modelsUrl: "https://api.runcrate.ai/v1/models", lastVerified: "2026-10-27", discovery: "hybrid", models: ["deepseek-ai/DeepSeek-V4.1-Flash", "deepseek-ai/DeepSeek-V4.1-Flash-1017", "zai-org/GLM-5.3-Flash", "zai-org/GLM-5.3", "openai/gpt-oss-120b", "Qwen/Qwen3.8-Flash"], note: "Only the six listed open models have a free allowance: one per GitHub account at least 30 days old, limited per session and per week, and shared by the workspace. Every other discovered model is paid; chat inference uses published per-token rates. When the allowance is used up or free capacity is busy, a workspace with credit continues as paid traffic; without credit, requests stop until the allowance resets or capacity frees up. Terms: https://www.runcrate.ai/inference/free" }), sambanova: openAi("https://api.sambanova.ai/v1", "https://cloud.sambanova.ai/apis", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.sambanova.ai/docs/en/get-started/api-keys-urls", modelsUrl: "https://api.sambanova.ai/v1/models", lastVerified: "2026-08-02" }), sparkdesk: openAi("https://spark-api-open.xf-yun.com/v1", "https://console.xfyun.cn", { verification: "official", documentationUrl: "https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html" }), agnes: openAi("https://apihub.agnes-ai.com/v1", "https://agnes-ai.com"), @@ -202,7 +210,7 @@ const LABELS: Record = { ai21: "AI21", baichuan: "Baichuan", crusoe: "Crusoe", deepinfra: "DeepInfra", deepseek: "DeepSeek", doubao: "Doubao", "freemodel-dev": "FreeModel.dev", sambanova: "SambaNova Cloud", nebius: "Nebius Token Factory", novita: "Novita", nscale: "Nscale", nvidia: "NVIDIA NIM", - publicai: "PublicAI", qoder: "Qoder", "qoder-cn": "Qoder CN", sensenova: "SenseNova", stepfun: "StepFun", vertex: "Google Vertex AI", + publicai: "PublicAI", qoder: "Qoder", "qoder-cn": "Qoder CN", runcrate: "Runcrate", sensenova: "SenseNova", stepfun: "StepFun", vertex: "Google Vertex AI", }; const referenceNote = "Reference entry only: no safe documented API integration is enabled. Configure it manually only with provider documentation; consumer-web cookies and anti-bot bypasses are intentionally unsupported."; diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 11138344887..f816f163027 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1048,7 +1048,7 @@ "provider-outbound.test.ts": "providers", "provider-runtime-fetch.test.ts": "providers", "provider-tls-profile.test.ts": "providers", "provider-payload.test.ts": "gui", "provider-quota-label-sanitize.test.ts": "providers", "provider-quota-observed-marker.test.ts": "providers", - "provider-quota-abliteration.test.ts": "providers", "provider-quota-ollama-cloud.test.ts": "providers", "provider-quota.test.ts": "providers", "provider-registry-parity.test.ts": "providers", "haiku-5-5-catalog.test.ts": "providers", + "provider-quota-abliteration.test.ts": "providers", "provider-quota-ollama-cloud.test.ts": "providers", "provider-quota.test.ts": "providers", "provider-registry-parity.test.ts": "providers", "free-directory-runcrate.test.ts": "providers", "haiku-5-5-catalog.test.ts": "providers", "provider-send-path-import.test.ts": "server", "provider-static-model-discovery.test.ts": "providers", "provider-subscription-cli.test.ts": "gui", "provider-workspace-auth.test.ts": "gui", "provider-workspace-data.test.ts": "gui", "provider-workspace-rail.test.ts": "gui", diff --git a/tests/providers/free-directory-runcrate.test.ts b/tests/providers/free-directory-runcrate.test.ts new file mode 100644 index 00000000000..bde88a96f09 --- /dev/null +++ b/tests/providers/free-directory-runcrate.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from "bun:test"; +import { FREE_PROVIDER_DIRECTORY } from "../../src/providers/free-directory"; +import { PROVIDER_REGISTRY } from "../../src/providers/registry"; + +// Sibling of provider-registry-parity.test.ts, which sits at the file-size ratchet threshold. +describe("free-provider directory: Runcrate", () => { + test("the Runcrate row bounds its free allowance and keeps the paid catalog discoverable", () => { + // The row accepts a key and discovers the full /v1/models catalog, which is mostly paid. Only + // six named models have a free allowance (runcrate.ai/inference/free and free_tier in /v1/models, + // checked 2026-10-27), so the note must carry those bounds instead of advertising the whole catalog as free. + const row = FREE_PROVIDER_DIRECTORY.find(entry => entry.id === "runcrate"); + expect(row).toMatchObject({ + label: "Runcrate", + adapter: "openai-chat", + baseUrl: "https://api.runcrate.ai/v1", + authKind: "key", + accessGroups: ["recurring-or-keyless"], + supportLevel: "experimental", + verification: "official", + documentationUrl: "https://www.runcrate.ai/docs/models/chat-completions", + modelsUrl: "https://api.runcrate.ai/v1/models", + dashboardUrl: "https://www.runcrate.ai/dashboard/api-keys", + discovery: "hybrid", + liveModels: true, + lastVerified: "2026-10-27", + }); + expect(row?.keyOptional).toBeUndefined(); + expect(row?.models).toEqual([ + "deepseek-ai/DeepSeek-V4.1-Flash", + "deepseek-ai/DeepSeek-V4.1-Flash-1017", + "zai-org/GLM-5.3-Flash", + "zai-org/GLM-5.3", + "openai/gpt-oss-120b", + "Qwen/Qwen3.8-Flash", + ]); + const note = row?.note ?? ""; + expect(note).toContain("Only the six listed open models have a free allowance"); + expect(note).toContain("GitHub account at least 30 days old"); + expect(note).toContain("per session and per week"); + expect(note).toContain("Every other discovered model is paid"); + expect(note).toContain("continues as paid traffic"); + expect(note).toContain("https://www.runcrate.ai/inference/free"); + // Directory-only: it must not become a canonical credential destination. + expect(PROVIDER_REGISTRY.some(entry => entry.id === "runcrate")).toBe(false); + }); +});