From 8123b97d20a0cccccc001bf535d604c460ea67b7 Mon Sep 17 00:00:00 2001 From: clementguarino06510-glitch Date: Mon, 31 Aug 2026 08:32:59 +0000 Subject: [PATCH] feat(embeddings): add orcarouter embedding provider Add OrcaRouter as a first-class embedding provider, mirroring the existing OpenAI wiring. OrcaRouter exposes an OpenAI-compatible embeddings API on the same endpoint as its chat/agent gateway, so the OpenAI SDK path applies with ORCAROUTER_API_KEY and an optional ORCAROUTER_API_BASE override (defaults to https://api.orcarouter.ai/v1). Update the provider union and credentials check in EmbeddingFactory and EmbeddingConfig, add docs and env.example entries, and cover the new provider with unit tests. --- .env.example | 7 ++++ docs/guides/embedding-models.md | 15 +++++++ src/store/embeddings/EmbeddingConfig.test.ts | 1 + src/store/embeddings/EmbeddingConfig.ts | 4 +- src/store/embeddings/EmbeddingFactory.test.ts | 42 +++++++++++++++++++ src/store/embeddings/EmbeddingFactory.ts | 37 +++++++++++++++- 6 files changed, 103 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 72cb423a..391ef95b 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,7 @@ # - gemini:gemini-embedding-exp-03-07 (Google Generative AI) # - aws:amazon.titan-embed-text-v1 # - microsoft:text-embedding-ada-002 +# - orcarouter:openai/text-embedding-3-small (OrcaRouter) DOCS_MCP_EMBEDDING_MODEL= # PostHog Analytics Configuration (Optional) @@ -44,6 +45,12 @@ AZURE_OPENAI_API_INSTANCE_NAME=your-instance AZURE_OPENAI_API_DEPLOYMENT_NAME=your-deployment AZURE_OPENAI_API_VERSION=2024-02-01 +# OrcaRouter Configuration +# Required for orcarouter provider +ORCAROUTER_API_KEY=your-orcarouter-api-key +# Optional: Override the default OrcaRouter endpoint (https://api.orcarouter.ai/v1) +# ORCAROUTER_API_BASE= + # Optional: Specify a custom directory to store the SQLite database file (documents.db). # If set, this path takes precedence over the default locations. # Default behavior (if unset): diff --git a/docs/guides/embedding-models.md b/docs/guides/embedding-models.md index ab40c2c6..02804803 100644 --- a/docs/guides/embedding-models.md +++ b/docs/guides/embedding-models.md @@ -14,6 +14,7 @@ If you leave the model empty but provide `OPENAI_API_KEY`, the server defaults t - `gemini:embedding-001` (Google Gemini) - `aws:amazon.titan-embed-text-v1` (AWS Bedrock) - `microsoft:text-embedding-ada-002` (Azure OpenAI) +- `orcarouter:openai/text-embedding-3-small` (OrcaRouter) - Or any OpenAI-compatible model name ## Provider Configuration @@ -34,6 +35,8 @@ Provider credentials use the provider-specific environment variables listed belo | `AZURE_OPENAI_API_INSTANCE_NAME` | Azure OpenAI instance name. | | `AZURE_OPENAI_API_DEPLOYMENT_NAME` | Azure OpenAI deployment name. | | `AZURE_OPENAI_API_VERSION` | Azure OpenAI API version. | +| `ORCAROUTER_API_KEY` | OrcaRouter API key for embeddings. | +| `ORCAROUTER_API_BASE` | Optional OrcaRouter endpoint override (defaults to `https://api.orcarouter.ai/v1`). | ### Examples @@ -114,6 +117,18 @@ DOCS_MCP_EMBEDDING_MODEL="microsoft:text-embedding-ada-002" \ npx @arabold/docs-mcp-server@latest ``` +#### OrcaRouter + +Use embeddings through the OrcaRouter OpenAI-compatible gateway. Model names follow the OrcaRouter model catalog (e.g., `openai/text-embedding-3-small`). + +```bash +ORCAROUTER_API_KEY="your-orcarouter-api-key" \ +DOCS_MCP_EMBEDDING_MODEL="orcarouter:openai/text-embedding-3-small" \ +npx @arabold/docs-mcp-server@latest +``` + +Set `ORCAROUTER_API_BASE` to override the default endpoint (`https://api.orcarouter.ai/v1`). + ## Changing the Embedding Model When you change the embedding model or vector dimension after initial setup, existing embedding vectors become semantically incompatible with the new configuration. The server detects this automatically by tracking the active model identity in a metadata table. diff --git a/src/store/embeddings/EmbeddingConfig.test.ts b/src/store/embeddings/EmbeddingConfig.test.ts index 7b3a054f..ce4ff5c5 100644 --- a/src/store/embeddings/EmbeddingConfig.test.ts +++ b/src/store/embeddings/EmbeddingConfig.test.ts @@ -249,6 +249,7 @@ describe("EmbeddingConfig", () => { "aws", "microsoft", "sagemaker", + "orcarouter", ]; it("should accept all valid providers", () => { diff --git a/src/store/embeddings/EmbeddingConfig.ts b/src/store/embeddings/EmbeddingConfig.ts index 515960a1..8ac1c351 100644 --- a/src/store/embeddings/EmbeddingConfig.ts +++ b/src/store/embeddings/EmbeddingConfig.ts @@ -18,7 +18,8 @@ export type EmbeddingProvider = | "gemini" | "aws" | "microsoft" - | "sagemaker"; + | "sagemaker" + | "orcarouter"; /** * Embedding model configuration parsed from environment variables. @@ -333,6 +334,7 @@ export class EmbeddingConfig { * - aws: AWS Bedrock models * - microsoft: Azure OpenAI * - sagemaker: AWS SageMaker hosted models + * - orcarouter: OrcaRouter (OpenAI-compatible AI gateway) * * @param modelSpec Model specification (e.g., "openai:text-embedding-3-small"), defaults to "text-embedding-3-small" * @returns Parsed embedding model configuration diff --git a/src/store/embeddings/EmbeddingFactory.test.ts b/src/store/embeddings/EmbeddingFactory.test.ts index 3da4e926..7118a7f6 100644 --- a/src/store/embeddings/EmbeddingFactory.test.ts +++ b/src/store/embeddings/EmbeddingFactory.test.ts @@ -34,6 +34,7 @@ beforeEach(() => { AZURE_OPENAI_API_INSTANCE_NAME: "test-instance", AZURE_OPENAI_API_DEPLOYMENT_NAME: "test-deployment", AZURE_OPENAI_API_VERSION: "2024-02-01", + ORCAROUTER_API_KEY: "test-orcarouter-key", }, }); }); @@ -150,6 +151,47 @@ describe("createEmbeddingModel", () => { ); }); + test("should create OrcaRouter embeddings via OpenAI-compatible endpoint", () => { + const model = createEmbeddingModel( + "orcarouter:openai/text-embedding-3-small", + runtimeConfig, + ); + expect(model).toBeInstanceOf(OpenAIEmbeddings); + expect(model).toMatchObject({ + modelName: "openai/text-embedding-3-small", + encodingFormat: "float", + }); + const clientConfig = (model as unknown as { clientConfig?: { baseURL?: string } }) + .clientConfig; + expect(clientConfig?.baseURL).toBe("https://api.orcarouter.ai/v1"); + }); + + test("should honor ORCAROUTER_API_BASE for OrcaRouter embeddings", () => { + vi.stubGlobal("process", { + env: { + ORCAROUTER_API_KEY: "test-orcarouter-key", + ORCAROUTER_API_BASE: "https://orcarouter.example.com/v1", + }, + }); + const model = createEmbeddingModel("orcarouter:test-model", runtimeConfig); + expect(model).toBeInstanceOf(OpenAIEmbeddings); + const clientConfig = (model as unknown as { clientConfig?: { baseURL?: string } }) + .clientConfig; + expect(clientConfig?.baseURL).toBe("https://orcarouter.example.com/v1"); + }); + + test("should throw MissingCredentialsError for OrcaRouter without ORCAROUTER_API_KEY", () => { + vi.stubGlobal("process", { + env: { + // Missing ORCAROUTER_API_KEY + }, + }); + + expect(() => + createEmbeddingModel("orcarouter:openai/text-embedding-3-small", runtimeConfig), + ).toThrow(MissingCredentialsError); + }); + test("should throw MissingCredentialsError for Azure OpenAI without required env vars", () => { // Override env to simulate missing Azure variables vi.stubGlobal("process", { diff --git a/src/store/embeddings/EmbeddingFactory.ts b/src/store/embeddings/EmbeddingFactory.ts index 37132fff..0f0ef4d6 100644 --- a/src/store/embeddings/EmbeddingFactory.ts +++ b/src/store/embeddings/EmbeddingFactory.ts @@ -22,7 +22,8 @@ export type EmbeddingProvider = | "gemini" | "aws" | "microsoft" - | "sagemaker"; + | "sagemaker" + | "orcarouter"; /** * Error thrown when an invalid or unsupported embedding provider is specified. @@ -31,7 +32,7 @@ export class UnsupportedProviderError extends Error { constructor(provider: string) { super( `❌ Unsupported embedding provider: ${provider}\n` + - " Supported providers: openai, vertex, gemini, aws, microsoft, sagemaker\n" + + " Supported providers: openai, vertex, gemini, aws, microsoft, sagemaker, orcarouter\n" + " See README.md for configuration options or run with --help for more details.", ); this.name = "UnsupportedProviderError"; @@ -93,6 +94,9 @@ export function areCredentialsAvailable(provider: EmbeddingProvider): boolean { ); } + case "orcarouter": + return !!process.env.ORCAROUTER_API_KEY; + default: return false; } @@ -110,6 +114,7 @@ export function areCredentialsAvailable(provider: EmbeddingProvider): boolean { * - Google GenAI (Gemini): GOOGLE_API_KEY * - AWS: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION (or BEDROCK_AWS_REGION) * - Microsoft: AZURE_OPENAI_API_KEY, AZURE_OPENAI_API_INSTANCE_NAME, AZURE_OPENAI_API_DEPLOYMENT_NAME, AZURE_OPENAI_API_VERSION + * - OrcaRouter: ORCAROUTER_API_KEY (and optionally ORCAROUTER_API_BASE) * * @param providerAndModel - The provider and model name in the format "provider:model_name" * or just "model_name" for OpenAI models. @@ -271,6 +276,34 @@ export function createEmbeddingModel( }); } + case "orcarouter": { + if (!process.env.ORCAROUTER_API_KEY) { + throw new MissingCredentialsError("orcarouter", ["ORCAROUTER_API_KEY"]); + } + // OrcaRouter exposes an OpenAI-compatible embeddings API on the same + // endpoint as its chat/agent gateway, so the OpenAI SDK path applies. + // The "float" encoding format keeps OpenAI-compatible providers that + // ignore the parameter from silently corrupting the returned vectors. + const config: Partial & { configuration?: ClientOptions } = + { + ...baseConfig, + modelName: model, + batchSize: 512, + timeout: requestTimeoutMs, + encodingFormat: "float", + }; + // Custom base URL if specified, otherwise the default OrcaRouter endpoint + const baseURL = process.env.ORCAROUTER_API_BASE || "https://api.orcarouter.ai/v1"; + config.configuration = { + baseURL, + timeout: requestTimeoutMs, + }; + return new OpenAIEmbeddings({ + ...config, + apiKey: process.env.ORCAROUTER_API_KEY, + }); + } + default: throw new UnsupportedProviderError(provider); }