Reorganize for multi-provider support (#48)
authorStefan Gasser <redacted>
Sat, 17 Jan 2026 20:07:39 +0000 (21:07 +0100)
committerGitHub <redacted>
Sat, 17 Jan 2026 20:07:39 +0000 (21:07 +0100)
* Reorganize for multi-provider support

Move LLM client to providers/ and rename proxy routes to openai:
- src/services/llm-client.ts → src/providers/openai-client.ts
- src/routes/proxy.ts → src/routes/openai.ts
- Rename proxyRoutes export to openaiRoutes

Prepares codebase for adding Anthropic provider support.

* Fix import sorting for biome check

12 files changed:
src/index.ts
src/pii/mask.test.ts
src/pii/mask.ts
src/providers/openai-client.ts [moved from src/services/llm-client.ts with 100% similarity]
src/routes/openai.test.ts [moved from src/routes/proxy.test.ts with 96% similarity]
src/routes/openai.ts [moved from src/routes/proxy.ts with 99% similarity]
src/secrets/detect.ts
src/secrets/mask.ts
src/secrets/multimodal.test.ts
src/services/decision.ts
src/utils/message-transform.test.ts
src/utils/message-transform.ts

index c04113557595bae6ed2a5fa1d298370df21ee8ba..88c2f69ee2bf03401af8b3893c462112708f5028 100644 (file)
@@ -8,7 +8,7 @@ import { getPIIDetector } from "./pii/detect";
 import { dashboardRoutes } from "./routes/dashboard";
 import { healthRoutes } from "./routes/health";
 import { infoRoutes } from "./routes/info";
-import { proxyRoutes } from "./routes/proxy";
+import { openaiRoutes } from "./routes/openai";
 import { getLogger } from "./services/logger";
 
 type Variables = {
@@ -42,7 +42,7 @@ app.get("/favicon.svg", (c) => {
 
 app.route("/", healthRoutes);
 app.route("/", infoRoutes);
-app.route("/openai/v1", proxyRoutes);
+app.route("/openai/v1", openaiRoutes);
 
 if (config.dashboard.enabled) {
   app.route("/dashboard", dashboardRoutes);
index fdd582e241c666c38f0cca6c3595e2c88412d790..7cac72c5887b571827699ff06689d51d133cbe6c 100644 (file)
@@ -1,6 +1,6 @@
 import { describe, expect, test } from "bun:test";
 import type { MaskingConfig } from "../config";
-import type { ChatMessage } from "../services/llm-client";
+import type { ChatMessage } from "../providers/openai-client";
 import { createPIIResult } from "../test-utils/detection-results";
 import type { PIIEntity } from "./detect";
 import {
index 13d1c13c56cbc96058b40fe117416754514e31fd..6133e7116d67d63d31174646b1e11b35f4c1cc8e 100644 (file)
@@ -1,5 +1,5 @@
 import type { MaskingConfig } from "../config";
-import type { ChatCompletionResponse, ChatMessage } from "../services/llm-client";
+import type { ChatCompletionResponse, ChatMessage } from "../providers/openai-client";
 import { resolveConflicts } from "../utils/conflict-resolver";
 import {
   createPlaceholderContext,
similarity index 96%
rename from src/routes/proxy.test.ts
rename to src/routes/openai.test.ts
index efebac346a2c64c35d6a447607b43dcea2a1536b..b9fe6185892a1248ee1062ab658dcd9849a0cdf0 100644 (file)
@@ -1,9 +1,9 @@
 import { describe, expect, test } from "bun:test";
 import { Hono } from "hono";
-import { proxyRoutes } from "./proxy";
+import { openaiRoutes } from "./openai";
 
 const app = new Hono();
-app.route("/openai/v1", proxyRoutes);
+app.route("/openai/v1", openaiRoutes);
 
 describe("POST /openai/v1/chat/completions", () => {
   test("returns 400 for missing messages", async () => {
similarity index 99%
rename from src/routes/proxy.ts
rename to src/routes/openai.ts
index 99b91b5f3cbd88d53c2f65a9f49c101bb346056e..b49a4ec90e4c9907fbcf82ebd366192a20133c8b 100644 (file)
@@ -5,16 +5,16 @@ import { proxy } from "hono/proxy";
 import { z } from "zod";
 import { getConfig, type MaskingConfig } from "../config";
 import { unmaskResponse as unmaskPIIResponse } from "../pii/mask";
-import { detectSecretsInMessages, type MessageSecretsResult } from "../secrets/detect";
-import { maskMessages as maskSecretsMessages, unmaskSecretsResponse } from "../secrets/mask";
-import { getRouter, type MaskDecision, type RoutingDecision } from "../services/decision";
 import {
   type ChatCompletionRequest,
   type ChatCompletionResponse,
   type ChatMessage,
   LLMError,
   type LLMResult,
-} from "../services/llm-client";
+} from "../providers/openai-client";
+import { detectSecretsInMessages, type MessageSecretsResult } from "../secrets/detect";
+import { maskMessages as maskSecretsMessages, unmaskSecretsResponse } from "../secrets/mask";
+import { getRouter, type MaskDecision, type RoutingDecision } from "../services/decision";
 import { logRequest, type RequestLogData } from "../services/logger";
 import { createUnmaskingStream } from "../services/stream-transformer";
 import { extractTextContent } from "../utils/content";
@@ -36,7 +36,7 @@ const ChatCompletionSchema = z
   })
   .passthrough();
 
-export const proxyRoutes = new Hono();
+export const openaiRoutes = new Hono();
 
 /**
  * Type guard for MaskDecision
@@ -83,7 +83,7 @@ function createErrorLogData(
 /**
  * POST /v1/chat/completions - OpenAI-compatible chat completion endpoint
  */
-proxyRoutes.post(
+openaiRoutes.post(
   "/chat/completions",
   zValidator("json", ChatCompletionSchema, (result, c) => {
     if (!result.success) {
@@ -474,7 +474,7 @@ function formatMessagesForLog(messages: ChatMessage[]): string {
 /**
  * Wildcard proxy for /models, /embeddings, /audio/*, /images/*, etc.
  */
-proxyRoutes.all("/*", (c) => {
+openaiRoutes.all("/*", (c) => {
   const { openai } = getRouter().getProvidersInfo();
   const path = c.req.path.replace(/^\/openai\/v1/, "");
 
index 36e5edd94b951a0349bedaa593b1e3cd6f955ba4..dfe106e467f4eed2179cfdb8a643d596eb871915 100644 (file)
@@ -1,5 +1,5 @@
 import type { SecretsDetectionConfig } from "../config";
-import type { ChatMessage } from "../services/llm-client";
+import type { ChatMessage } from "../providers/openai-client";
 import type { ContentPart } from "../utils/content";
 import { patternDetectors } from "./patterns";
 import type {
index 0c8cf19836ed350b7303699005c2d8ab363d6894..2d3139c75226305ffcac19c57d4457659dbe4f51 100644 (file)
@@ -1,4 +1,4 @@
-import type { ChatCompletionResponse, ChatMessage } from "../services/llm-client";
+import type { ChatCompletionResponse, ChatMessage } from "../providers/openai-client";
 import { resolveOverlaps } from "../utils/conflict-resolver";
 import {
   createPlaceholderContext,
index e5a4a2aa933401d5f9e16557e7c1a875b743176a..6a23a8e75aabb25a67095556d91ddf86787da099 100644 (file)
@@ -1,7 +1,7 @@
 import { describe, expect, test } from "bun:test";
 import type { PIIDetectionResult, PIIEntity } from "../pii/detect";
 import { maskMessages } from "../pii/mask";
-import type { ChatMessage } from "../services/llm-client";
+import type { ChatMessage } from "../providers/openai-client";
 import type { ContentPart } from "../utils/content";
 
 /**
index a6bef8b255f13eb5921c3c669c0c09ac69654285..a07286d8b800705fac9522f0004e4041d29c2884 100644 (file)
@@ -1,9 +1,9 @@
 import { type Config, getConfig } from "../config";
 import { getPIIDetector, type PIIDetectionResult } from "../pii/detect";
 import { createMaskingContext, maskMessages } from "../pii/mask";
+import { type ChatMessage, LLMClient } from "../providers/openai-client";
 import type { MessageSecretsResult } from "../secrets/detect";
 import type { PlaceholderContext } from "../utils/message-transform";
-import { type ChatMessage, LLMClient } from "./llm-client";
 
 /**
  * Routing decision result for route mode
index 0c68abcb597ce032b3d67a509ccf2d65a0b6004e..72c4359b25a8fc950bc893a33859a37c080db1dd 100644 (file)
@@ -1,5 +1,5 @@
 import { describe, expect, test } from "bun:test";
-import type { ChatMessage } from "../services/llm-client";
+import type { ChatMessage } from "../providers/openai-client";
 import type { Span } from "./conflict-resolver";
 import {
   createPlaceholderContext,
index 424d152d8e252168fbe153af0eab713c0f47762c..408b4c154a82f2ef4b8d5189350051d965136864 100644 (file)
@@ -9,7 +9,7 @@
  * This module provides shared infrastructure to avoid duplication.
  */
 
-import type { ChatMessage } from "../services/llm-client";
+import type { ChatMessage } from "../providers/openai-client";
 import type { Span } from "./conflict-resolver";
 import type { ContentPart } from "./content";
 import { findPartialPlaceholderStart } from "./placeholders";
git clone https://git.99rst.org/PROJECT