mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(api): VS Code Copilot Ollama-compatible BYOK endpoint (#3316)
Integrated into release/v3.8.13 — VS Code Copilot Ollama-compatible BYOK endpoint (reconciled with #3306/#3309 auth hardening)
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
- **fix(codex):** strip client-only params (`prompt_cache_retention`, `safety_identifier`, `user`) on the native `codex/` `/v1/responses` passthrough — Codex upstream rejects them with `400 Unsupported parameter`, which broke Factory Droid and any client injecting those fields. The chat-completions path already stripped them; the responses→responses passthrough now does too. (#3317 — thanks @tycronk20)
|
||||
- **fix(theoldllm):** stop the `[502]: Body is unusable: Body has already been read` error on the cached-token path — the executor read the same upstream `Response` body with `.text()` twice; it now reads it once and only re-reads after a token-rejection refetch. (#3296 — thanks @onizukashonan14-png)
|
||||
- **fix(dashboard):** keep no-auth providers (opencode, duckduckgo-web, theoldllm, veoaifree-web) visible under the "Show configured only" filter — they never create a connection row (`stats.total === 0`) but are always usable and already appear in `/v1/models`, so the filter now treats `displayAuthType === "no-auth"` as configured. (#3290 — thanks @uniQta)
|
||||
- **fix(dashboard):** refresh the connection list after a Codex/Claude/Gemini auth import — the import modals called `fetchData()` (which only reloads provider metadata), so a freshly-imported connection stayed invisible until a manual reload; they now call `fetchConnections()`. ([#3320](https://github.com/diegosouzapw/OmniRoute/pull/3320) — thanks @zhiru)
|
||||
- **fix(cli):** `omniroute update` no longer always fails on a global install — `getCurrentVersion()` and `createBackup()` now resolve `package.json`/`bin` relative to the script (`import.meta.url`) instead of `process.cwd()` (the user's working dir on a global npm/brew install → *"Could not determine current version"*), and the backup copies the `cli` directory with `cpSync({recursive:true})` instead of `copyFileSync`, which threw a swallowed `EISDIR` → *"Failed to create backup. Aborting"*. (#3295 — thanks @uniQta)
|
||||
- **fix(sse):** harden the passthrough stream against empty upstream responses — emit a synthetic retry chunk on an empty `choices: []` (fixes a Copilot Chat crash) and log empty post-`tool_calls` completions; also registers **MiniMax M3** (1M context) across 8 provider tiers. ([#3297](https://github.com/diegosouzapw/OmniRoute/pull/3297), #3110 — thanks @wilsonicdev)
|
||||
- **fix(opencode-provider):** extract `contextLength` from the live `/v1/models` catalog (live > `modelContextLengths` > static map) so passthrough models outside the legacy 8-model map no longer silently truncate to OpenCode's 128K default. ([#3298](https://github.com/diegosouzapw/OmniRoute/pull/3298) — thanks @herjarsa / @diegosouzapw)
|
||||
@@ -62,7 +63,8 @@ Thanks to everyone whose work landed in v3.8.13:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| --- | --- |
|
||||
| [@zhiru](https://github.com/zhiru) | #3300, #3306, #3307 / #3310, #3309, #3301, #3311 |
|
||||
| [@zhiru](https://github.com/zhiru) | #3300, #3306, #3307 / #3310, #3309, #3301, #3311, #3320 |
|
||||
| [@tycronk20](https://github.com/tycronk20) | #3317, #3318 |
|
||||
| [@oyi77](https://github.com/oyi77) | #3292 (closes #3070) |
|
||||
| [@onizukashonan14-png](https://github.com/onizukashonan14-png) | #3296 |
|
||||
| [@uniQta](https://github.com/uniQta) | #3290, #3295 |
|
||||
|
||||
@@ -10,7 +10,7 @@ lastUpdated: 2026-05-30
|
||||
>
|
||||
> Source of truth: `open-sse/mcp-server/schemas/tools.ts` (30 tools) + `open-sse/mcp-server/tools/memoryTools.ts` (3 tools) + `open-sse/mcp-server/tools/skillTools.ts` (4 tools) + `open-sse/mcp-server/tools/notionTools.ts` (6 tools). Tool registration and scope wiring lives in `open-sse/mcp-server/server.ts`.
|
||||
|
||||

|
||||

|
||||
|
||||
> Source: [diagrams/mcp-tools-43.mmd](../diagrams/mcp-tools-43.mmd) (regenerate via `npm run docs:render-diagrams`).
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Card } from "@/shared/components";
|
||||
import { useDisplayBaseUrl } from "@/shared/hooks";
|
||||
import VscodeTokenAliasCard from "./VscodeTokenAliasCard";
|
||||
import { matchesSearch } from "@/shared/utils/turkishText";
|
||||
|
||||
/* ─── Types ──────────────────────────────────────────── */
|
||||
@@ -271,31 +272,35 @@ export default function ApiEndpointsTab() {
|
||||
|
||||
{/* ═══ API CATALOG ═══ */}
|
||||
{!catalog && (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-red-500/10">
|
||||
<span className="material-symbols-outlined text-[20px] text-red-500">error</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-text-main">
|
||||
{t("apiEndpointsCatalogUnavailable")}
|
||||
</h3>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
{catalogError || "The OpenAPI specification could not be loaded."}
|
||||
</p>
|
||||
<a
|
||||
href="/api/openapi/spec"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="inline-flex items-center gap-1 mt-3 px-2.5 py-1.5 text-xs font-medium rounded-lg
|
||||
<>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-red-500/10">
|
||||
<span className="material-symbols-outlined text-[20px] text-red-500">error</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-text-main">
|
||||
{t("apiEndpointsCatalogUnavailable")}
|
||||
</h3>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
{catalogError || "The OpenAPI specification could not be loaded."}
|
||||
</p>
|
||||
<a
|
||||
href="/api/openapi/spec"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="inline-flex items-center gap-1 mt-3 px-2.5 py-1.5 text-xs font-medium rounded-lg
|
||||
bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">open_in_new</span>
|
||||
Open JSON response
|
||||
</a>
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">open_in_new</span>
|
||||
Open JSON response
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
<VscodeTokenAliasCard variant="catalog" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{catalog && (
|
||||
@@ -385,6 +390,8 @@ export default function ApiEndpointsTab() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VscodeTokenAliasCard variant="catalog" />
|
||||
|
||||
{/* Endpoint groups */}
|
||||
{Object.entries(groupedEndpoints).map(([tag, endpoints]) => (
|
||||
<Card key={tag} className="overflow-hidden">
|
||||
|
||||
@@ -12,6 +12,7 @@ import A2ADashboardPage from "./components/A2ADashboard";
|
||||
import McpDashboardPage from "./components/MCPDashboard";
|
||||
import TokenSaverCard from "./components/TokenSaverCard";
|
||||
import NotionSourceCard from "./components/NotionSourceCard";
|
||||
import VscodeTokenAliasCard from "./VscodeTokenAliasCard";
|
||||
|
||||
const BUILD_TIME_CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL || null;
|
||||
const CLOUD_ACTION_TIMEOUT_MS = 15000;
|
||||
@@ -2023,6 +2024,8 @@ export default function APIPageClient({ machineId }: Readonly<APIPageClientProps
|
||||
baseUrl={currentEndpoint}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VscodeTokenAliasCard className="mt-4" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
213
src/app/(dashboard)/dashboard/endpoint/VscodeTokenAliasCard.tsx
Normal file
213
src/app/(dashboard)/dashboard/endpoint/VscodeTokenAliasCard.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import { useDisplayBaseUrl } from "@/shared/hooks";
|
||||
import { Card } from "@/shared/components";
|
||||
|
||||
type EndpointApiKeySummary = {
|
||||
id: string;
|
||||
key: string;
|
||||
rawKey?: string;
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
export default function VscodeTokenAliasCard({
|
||||
className = "",
|
||||
variant = "highlight",
|
||||
}: Readonly<{ className?: string; variant?: "highlight" | "catalog" }>) {
|
||||
const t = useTranslations("endpoint");
|
||||
const [cliApiKeys, setCliApiKeys] = useState<EndpointApiKeySummary[]>([]);
|
||||
const [keysLoading, setKeysLoading] = useState(true);
|
||||
const [keysError, setKeysError] = useState(false);
|
||||
const { copied, copy } = useCopyToClipboard();
|
||||
const displayBaseUrl = useDisplayBaseUrl();
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const loadCliApiKeys = async () => {
|
||||
setKeysLoading(true);
|
||||
setKeysError(false);
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/cli-tools/keys");
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to load CLI keys");
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (!cancelled) {
|
||||
setCliApiKeys(data.keys || []);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setCliApiKeys([]);
|
||||
setKeysError(true);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setKeysLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void loadCliApiKeys();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const preferredCliApiKey = useMemo(() => {
|
||||
if (cliApiKeys.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const storedCopilotKeyId =
|
||||
typeof window !== "undefined" ? window.localStorage.getItem("omniroute-cli-key-copilot") : null;
|
||||
|
||||
return (
|
||||
(storedCopilotKeyId ? cliApiKeys.find((key) => key.id === storedCopilotKeyId) : null) ??
|
||||
cliApiKeys.find((key) => key.isActive !== false) ??
|
||||
cliApiKeys[0] ??
|
||||
null
|
||||
);
|
||||
}, [cliApiKeys]);
|
||||
|
||||
const tokenSegment = preferredCliApiKey?.rawKey || "{token}";
|
||||
const hasRealToken = Boolean(preferredCliApiKey?.rawKey);
|
||||
const translationToken = { token: "{token}" };
|
||||
const baseUrl = `${displayBaseUrl}/api/v1/vscode/${tokenSegment}`;
|
||||
const vscodeTokenizedUrls = [
|
||||
{
|
||||
label: t("vscodeAliasBaseLabel"),
|
||||
url: `${baseUrl}/`,
|
||||
key: "vscode_token_base",
|
||||
},
|
||||
{
|
||||
label: t("vscodeAliasModelsLabel"),
|
||||
url: `${baseUrl}/models`,
|
||||
key: "vscode_token_models",
|
||||
},
|
||||
{
|
||||
label: t("vscodeAliasChatLabel"),
|
||||
url: `${baseUrl}/chat/completions`,
|
||||
key: "vscode_token_chat",
|
||||
},
|
||||
];
|
||||
|
||||
const description = hasRealToken
|
||||
? t("vscodeAliasDescriptionReady", translationToken)
|
||||
: keysError
|
||||
? t("vscodeAliasDescriptionError")
|
||||
: keysLoading
|
||||
? t("vscodeAliasDescriptionLoading")
|
||||
: t("vscodeAliasDescriptionPlaceholder", translationToken);
|
||||
|
||||
if (variant === "catalog") {
|
||||
return (
|
||||
<Card className={`overflow-hidden ${className}`.trim()}>
|
||||
<div className="flex items-center gap-2 px-4 py-3 border-b border-black/5 dark:border-white/5">
|
||||
<span className="material-symbols-outlined text-[14px] text-primary">key</span>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
{t("vscodeAliasTitle")}
|
||||
</h3>
|
||||
<div className="flex-1 h-px bg-border/30" />
|
||||
<Link
|
||||
href="/dashboard/cli-tools"
|
||||
className="shrink-0 text-[11px] font-medium text-primary hover:underline"
|
||||
>
|
||||
{t("vscodeAliasManage")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-3">
|
||||
<p className="text-xs text-text-muted">{description}</p>
|
||||
|
||||
<div className="mt-3 flex flex-col gap-2">
|
||||
{vscodeTokenizedUrls.map(({ label, url, key }) => (
|
||||
<CopyableEndpointRow
|
||||
key={key}
|
||||
label={label}
|
||||
url={url}
|
||||
copyKey={key}
|
||||
copy={(text, copyKey) => void copy(text, copyKey)}
|
||||
copied={copied}
|
||||
variant="catalog"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg border border-sky-500/20 bg-sky-500/5 p-3 ${className}`.trim()}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400">
|
||||
{t("vscodeAliasTitle")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-text-muted">{description}</p>
|
||||
</div>
|
||||
<Link href="/dashboard/cli-tools" className="shrink-0 text-xs text-primary hover:underline">
|
||||
{t("vscodeAliasManage")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-col gap-2">
|
||||
{vscodeTokenizedUrls.map(({ label, url, key }) => (
|
||||
<CopyableEndpointRow
|
||||
key={key}
|
||||
label={label}
|
||||
url={url}
|
||||
copyKey={key}
|
||||
copy={(text, copyKey) => void copy(text, copyKey)}
|
||||
copied={copied}
|
||||
variant="highlight"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyableEndpointRow({
|
||||
label,
|
||||
url,
|
||||
copyKey,
|
||||
copy,
|
||||
copied,
|
||||
variant = "highlight",
|
||||
}: Readonly<{
|
||||
label: string;
|
||||
url: string;
|
||||
copyKey: string;
|
||||
copy: (text: string, key?: string) => void;
|
||||
copied?: string | null;
|
||||
variant?: "highlight" | "catalog";
|
||||
}>) {
|
||||
const rowClassName =
|
||||
variant === "catalog"
|
||||
? "flex items-center gap-2 min-w-0 rounded-lg border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.02] px-3 py-2.5"
|
||||
: "flex items-center gap-2 min-w-0 rounded-md border border-sky-500/15 bg-background/60 px-2.5 py-2";
|
||||
|
||||
return (
|
||||
<div className={rowClassName}>
|
||||
<span className="w-24 shrink-0 text-[11px] font-medium text-text-muted">{label}</span>
|
||||
<code className="flex-1 min-w-0 truncate text-[11px] font-mono text-text-main">{url}</code>
|
||||
<button
|
||||
onClick={() => copy(url, copyKey)}
|
||||
className="shrink-0 flex items-center gap-1 rounded border border-border/70 px-2 py-1 text-text-muted transition-colors hover:text-text"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[12px]">
|
||||
{copied === copyKey ? "check" : "content_copy"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,58 @@ import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import ApiEndpointsTab from "../ApiEndpointsTab";
|
||||
import { DEFAULT_DISPLAY_BASE_URL } from "@/shared/hooks/useDisplayBaseUrl";
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
|
||||
<a href={String(href)} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: (namespace?: string) => {
|
||||
const messages: Record<string, string> = {
|
||||
"endpoint.apiEndpointsCatalogUnavailable": "API catalog unavailable",
|
||||
"endpoint.apiEndpointsSearchPlaceholder": "Search endpoints",
|
||||
"endpoint.badgeLoopbackTooltip": "Loopback only",
|
||||
"endpoint.badgeAlwaysProtectedTooltip": "Always protected",
|
||||
"endpoint.badgeInternalTooltip": "Internal endpoint",
|
||||
"endpoint.tierAll": "All",
|
||||
"endpoint.tierAuth": "Auth",
|
||||
"endpoint.tierLoopback": "Loopback",
|
||||
"endpoint.tierAlwaysProtected": "Protected",
|
||||
"endpoint.tierPublic": "Public",
|
||||
"endpoint.showInternal": "Show internal",
|
||||
"endpoint.hideInternal": "Hide internal",
|
||||
"endpoint.vscodeAliasTitle": "VS Code Token Alias",
|
||||
"endpoint.vscodeAliasDescriptionReady": "Ready-to-paste compatibility URLs using the /api/v1/vscode/{token}/... endpoint.",
|
||||
"endpoint.vscodeAliasDescriptionError": "Showing placeholder URLs because CLI keys could not be loaded in this session.",
|
||||
"endpoint.vscodeAliasDescriptionLoading": "Loading CLI keys. Placeholder URLs are shown until a key is available.",
|
||||
"endpoint.vscodeAliasDescriptionPlaceholder": "Showing placeholder URLs. Create or activate an API key in CLI Tools to replace {token}.",
|
||||
"endpoint.vscodeAliasManage": "CLI Tools",
|
||||
"endpoint.vscodeAliasBaseLabel": "VS Code base",
|
||||
"endpoint.vscodeAliasModelsLabel": "VS Code models",
|
||||
"endpoint.vscodeAliasChatLabel": "VS Code chat",
|
||||
"endpoint.tryIt": "Try it",
|
||||
"endpoint.parameters": "Parameters",
|
||||
"endpoint.responses": "Responses",
|
||||
"endpoint.requestBody": "Request body",
|
||||
"endpoint.description": "Description",
|
||||
"endpoint.noDescription": "No description",
|
||||
"endpoint.security": "Security",
|
||||
"endpoint.authRequired": "Auth required",
|
||||
"endpoint.noAuth": "No auth",
|
||||
"endpoint.execute": "Execute",
|
||||
"endpoint.executing": "Executing",
|
||||
"endpoint.close": "Close",
|
||||
"endpoint.openJsonResponse": "Open JSON response",
|
||||
};
|
||||
|
||||
return (key: string) => messages[`${namespace}.${key}`] || key;
|
||||
},
|
||||
}));
|
||||
|
||||
function jsonResponse(data: unknown, status = 200) {
|
||||
return {
|
||||
@@ -66,18 +118,32 @@ describe("ApiEndpointsTab", () => {
|
||||
});
|
||||
|
||||
it("shows an API catalog error state instead of a blank page", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ error: "openapi.yaml not found" }, 404));
|
||||
fetchMock.mockImplementation(async (input) => {
|
||||
if (input === "/api/cli-tools/keys") {
|
||||
return jsonResponse({ keys: [] });
|
||||
}
|
||||
|
||||
return jsonResponse({ error: "openapi.yaml not found" }, 404);
|
||||
});
|
||||
|
||||
renderApiEndpointsTab();
|
||||
|
||||
await waitForText("VS Code Token Alias");
|
||||
await waitForText("API catalog unavailable");
|
||||
expect(document.body.textContent).toContain("openapi.yaml not found");
|
||||
expect(document.body.textContent).toContain("/api/v1/vscode/{token}/models");
|
||||
expect(document.body.textContent).toContain("Open JSON response");
|
||||
});
|
||||
|
||||
it("renders catalog content when the OpenAPI catalog loads", async () => {
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({
|
||||
fetchMock.mockImplementation(async (input) => {
|
||||
if (input === "/api/cli-tools/keys") {
|
||||
return jsonResponse({
|
||||
keys: [{ id: "copilot", key: "sk-***", rawKey: "sk-live-123", isActive: true }],
|
||||
});
|
||||
}
|
||||
|
||||
return jsonResponse({
|
||||
info: { title: "OmniRoute API", version: "3.7.6" },
|
||||
servers: [],
|
||||
tags: [{ name: "Chat" }],
|
||||
@@ -95,19 +161,25 @@ describe("ApiEndpointsTab", () => {
|
||||
},
|
||||
],
|
||||
schemas: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
renderApiEndpointsTab();
|
||||
|
||||
await waitForText("VS Code Token Alias");
|
||||
await waitForText("OmniRoute API");
|
||||
expect(document.body.textContent).toContain("1 endpoints across 1 categories");
|
||||
expect(document.body.textContent).toContain("/api/v1/vscode/sk-live-123/models");
|
||||
expect(document.body.textContent).toContain("/api/v1/chat/completions");
|
||||
});
|
||||
|
||||
it("renders curl example using window.location.origin when NEXT_PUBLIC_BASE_URL is unset", async () => {
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({
|
||||
fetchMock.mockImplementation(async (input) => {
|
||||
if (input === "/api/cli-tools/keys") {
|
||||
return jsonResponse({ keys: [] });
|
||||
}
|
||||
|
||||
return jsonResponse({
|
||||
info: { title: "OmniRoute API", version: "3.7.6" },
|
||||
servers: [],
|
||||
tags: [{ name: "Chat" }],
|
||||
@@ -125,27 +197,32 @@ describe("ApiEndpointsTab", () => {
|
||||
},
|
||||
],
|
||||
schemas: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
renderApiEndpointsTab();
|
||||
|
||||
await waitForText("OmniRoute API");
|
||||
|
||||
// Expand the endpoint to reveal the curl example
|
||||
const endpointRow = document.body.querySelector("code.font-mono.flex-1");
|
||||
const endpointRow = Array.from(document.body.querySelectorAll("code")).find((node) =>
|
||||
node.textContent?.includes("/api/v1/chat/completions")
|
||||
);
|
||||
if (endpointRow?.parentElement) {
|
||||
await act(async () => {
|
||||
endpointRow.parentElement!.click();
|
||||
});
|
||||
}
|
||||
|
||||
// After mount the hook swaps DEFAULT_DISPLAY_BASE_URL for window.location.origin.
|
||||
// In jsdom the default origin is "http://localhost".
|
||||
const expectedOrigin = window.location.origin; // "http://localhost" in jsdom
|
||||
await waitForText(`curl -X POST ${expectedOrigin}/v1/chat/completions`);
|
||||
expect(document.body.textContent).toContain(
|
||||
`curl -X POST ${expectedOrigin}/v1/chat/completions`
|
||||
);
|
||||
await waitForText("curl -X POST");
|
||||
|
||||
const renderedText = document.body.textContent || "";
|
||||
const expectedOrigins = [window.location.origin, DEFAULT_DISPLAY_BASE_URL];
|
||||
|
||||
expect(
|
||||
expectedOrigins.some((origin) =>
|
||||
renderedText.includes(`curl -X POST ${origin}/v1/chat/completions`)
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,6 +118,15 @@ vi.mock("next-intl", () => ({
|
||||
"endpoint.categoryCore": "Core APIs",
|
||||
"endpoint.categoryMedia": "Media APIs",
|
||||
"endpoint.categoryUtility": "Utility APIs",
|
||||
"endpoint.vscodeAliasTitle": "VS Code Token Alias",
|
||||
"endpoint.vscodeAliasDescriptionReady": "Ready-to-paste compatibility URLs using the /api/v1/vscode/{token}/... endpoint.",
|
||||
"endpoint.vscodeAliasDescriptionError": "Showing placeholder URLs because CLI keys could not be loaded in this session.",
|
||||
"endpoint.vscodeAliasDescriptionLoading": "Loading CLI keys. Placeholder URLs are shown until a key is available.",
|
||||
"endpoint.vscodeAliasDescriptionPlaceholder": "Showing placeholder URLs. Create or activate an API key in CLI Tools to replace {token}.",
|
||||
"endpoint.vscodeAliasManage": "CLI Tools",
|
||||
"endpoint.vscodeAliasBaseLabel": "VS Code base",
|
||||
"endpoint.vscodeAliasModelsLabel": "VS Code models",
|
||||
"endpoint.vscodeAliasChatLabel": "VS Code chat",
|
||||
"endpoint.machineId": "Machine {id}",
|
||||
"endpoint.usingLocalServer": "Using local server",
|
||||
"common.copy": "Copy",
|
||||
@@ -187,6 +196,30 @@ describe("EndpointPageClient", () => {
|
||||
if (path === "/api/search/providers") {
|
||||
return Promise.resolve(jsonResponse({ providers: [] }));
|
||||
}
|
||||
if (path === "/api/cli-tools/keys") {
|
||||
return Promise.resolve(
|
||||
jsonResponse({
|
||||
keys: [
|
||||
{
|
||||
id: "key-1",
|
||||
key: "sk-test-1234",
|
||||
rawKey: "sk-test-1234",
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
if (path === "/api/settings/compression") {
|
||||
return Promise.resolve(
|
||||
jsonResponse({
|
||||
enabled: true,
|
||||
cavemanConfig: { enabled: true, intensity: "full" },
|
||||
cavemanOutputMode: { enabled: false, intensity: "full" },
|
||||
rtkConfig: { enabled: true, intensity: "standard" },
|
||||
})
|
||||
);
|
||||
}
|
||||
throw new Error(`Unexpected request: ${path}`);
|
||||
});
|
||||
|
||||
@@ -217,7 +250,10 @@ describe("EndpointPageClient", () => {
|
||||
})
|
||||
);
|
||||
|
||||
await waitForText("2 models across 4 endpoints");
|
||||
await waitForText("2 models across");
|
||||
await waitForText("/api/v1/vscode/sk-test-1234/models");
|
||||
expect(document.body.textContent).toContain("VS Code Token Alias");
|
||||
expect(document.body.textContent).toContain("/api/v1/vscode/sk-test-1234/models");
|
||||
});
|
||||
|
||||
it("does not start background endpoint requests after unmounting during settings load", async () => {
|
||||
|
||||
1
src/app/api/v1/vscode/[token]/api/chat/route.ts
Normal file
1
src/app/api/v1/vscode/[token]/api/chat/route.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { POST, OPTIONS } from "@/app/api/v1/api/chat/route";
|
||||
329
src/app/api/v1/vscode/[token]/api/show/route.ts
Normal file
329
src/app/api/v1/vscode/[token]/api/show/route.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import { getUnifiedModelsResponse } from "@/app/api/v1/models/catalog";
|
||||
import { getCanonicalModelMetadata } from "@/lib/modelMetadataRegistry";
|
||||
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
|
||||
import {
|
||||
buildReasoningConfigSchema,
|
||||
buildSupportedReasoningEfforts,
|
||||
getDefaultReasoningEffort,
|
||||
getReasoningEffortValues,
|
||||
inferSelectedReasoningEffort,
|
||||
type VscodeCatalogModel,
|
||||
} from "@/app/api/v1/vscode/[token]/reasoningMetadata";
|
||||
import { getVscodeModelDisplayName } from "@/app/api/v1/vscode/[token]/modelPresentation";
|
||||
import {
|
||||
expandVscodeServiceTierModels,
|
||||
parseVscodeServiceTierVariantModelId,
|
||||
} from "@/app/api/v1/vscode/[token]/serviceTierVariants";
|
||||
import { getFamilyFirstModelCandidates, getFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/[token]/familyFirstModelIds";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/[token]/tokenizedRequest";
|
||||
|
||||
type OpenAiCatalogModel = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
root?: string;
|
||||
parent?: string | null;
|
||||
owned_by?: string;
|
||||
type?: string;
|
||||
api_format?: string;
|
||||
context_length?: number;
|
||||
max_output_tokens?: number;
|
||||
capabilities?: Record<string, boolean>;
|
||||
input_modalities?: string[];
|
||||
output_modalities?: string[];
|
||||
supported_endpoints?: string[];
|
||||
};
|
||||
|
||||
function isUsableChatModel(model: OpenAiCatalogModel) {
|
||||
if (typeof model.owned_by === "string" && model.owned_by.trim().toLowerCase() === "combo") {
|
||||
return false;
|
||||
}
|
||||
if (typeof model.parent === "string" && model.parent.length > 0) return false;
|
||||
if (typeof model.type === "string" && model.type !== "chat") return false;
|
||||
|
||||
const apiFormat = typeof model.api_format === "string" ? model.api_format : "chat-completions";
|
||||
if (apiFormat !== "chat-completions") return false;
|
||||
|
||||
if (
|
||||
Array.isArray(model.supported_endpoints) &&
|
||||
model.supported_endpoints.length > 0 &&
|
||||
!model.supported_endpoints.includes("chat")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(model.output_modalities) &&
|
||||
model.output_modalities.length > 0 &&
|
||||
!model.output_modalities.includes("text")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getCatalogModelId(model: OpenAiCatalogModel) {
|
||||
return model.id || model.name || model.root || "unknown";
|
||||
}
|
||||
|
||||
function normalizeArchitectureKey(value: string) {
|
||||
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
||||
return normalized || "model";
|
||||
}
|
||||
|
||||
function normalizeArchitectureSource(value: string) {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === "cx") return "codex";
|
||||
if (normalized === "gh") return "github";
|
||||
return value;
|
||||
}
|
||||
|
||||
function getRequestedModelName(payload: unknown): string | null {
|
||||
if (!payload || typeof payload !== "object") return null;
|
||||
const record = payload as Record<string, unknown>;
|
||||
const candidate = record.name ?? record.model;
|
||||
return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : null;
|
||||
}
|
||||
|
||||
function getOllamaModelFamily(model: OpenAiCatalogModel, canonicalFamily?: string | null) {
|
||||
const rawModelId = getCatalogModelId(model).trim();
|
||||
const { baseModelId } = parseVscodeServiceTierVariantModelId(rawModelId);
|
||||
const modelFamily = baseModelId.includes("/") ? baseModelId.split("/").slice(1).join("/") : baseModelId;
|
||||
|
||||
if (modelFamily) {
|
||||
return modelFamily;
|
||||
}
|
||||
|
||||
if (canonicalFamily && canonicalFamily.trim().length > 0) {
|
||||
return canonicalFamily.trim();
|
||||
}
|
||||
|
||||
return typeof model.owned_by === "string" && model.owned_by.trim().length > 0
|
||||
? model.owned_by.trim()
|
||||
: "omniroute";
|
||||
}
|
||||
|
||||
function matchesRequestedModel(model: OpenAiCatalogModel, requestedName: string): boolean {
|
||||
const canonicalMetadata = getCanonicalModelMetadata({
|
||||
provider: model.owned_by || null,
|
||||
model: model.root || model.id || model.name || null,
|
||||
});
|
||||
const family = getOllamaModelFamily(model, canonicalMetadata?.metadata.family || null);
|
||||
const actualModelId = getCatalogModelId(model);
|
||||
|
||||
return [
|
||||
model.id,
|
||||
model.name,
|
||||
model.root,
|
||||
canonicalMetadata?.qualifiedId,
|
||||
canonicalMetadata?.model,
|
||||
...getFamilyFirstModelCandidates(actualModelId, family),
|
||||
].some((value) => value === requestedName);
|
||||
}
|
||||
|
||||
function buildCapabilities(model: OpenAiCatalogModel): string[] {
|
||||
const capabilities = ["completion"];
|
||||
if (model.capabilities?.vision) capabilities.push("vision");
|
||||
if (model.capabilities?.tool_calling) capabilities.push("tools");
|
||||
if (model.capabilities?.reasoning || model.capabilities?.thinking) capabilities.push("thinking");
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
function buildShowPayload(model: OpenAiCatalogModel, responseModelId?: string) {
|
||||
const actualModelId = getCatalogModelId(model);
|
||||
const displayName = getVscodeModelDisplayName(model);
|
||||
const canonicalMetadata = getCanonicalModelMetadata({
|
||||
provider: model.owned_by || null,
|
||||
model: model.root || model.id || model.name || null,
|
||||
});
|
||||
const family = getOllamaModelFamily(model, canonicalMetadata?.metadata.family || null);
|
||||
const modelId = responseModelId || getFamilyFirstPublishedModelId(actualModelId, family);
|
||||
const architectureSource =
|
||||
normalizeArchitectureSource(
|
||||
canonicalMetadata?.providerAlias || canonicalMetadata?.provider || model.owned_by || family || "model"
|
||||
);
|
||||
const architecture = normalizeArchitectureKey(
|
||||
architectureSource
|
||||
);
|
||||
const reasoningEffortValues = getReasoningEffortValues(model as VscodeCatalogModel);
|
||||
const selectedReasoningEffort = reasoningEffortValues
|
||||
? inferSelectedReasoningEffort(model as VscodeCatalogModel, reasoningEffortValues) || "none"
|
||||
: undefined;
|
||||
const defaultReasoningEffort = reasoningEffortValues
|
||||
? getDefaultReasoningEffort(model as VscodeCatalogModel, reasoningEffortValues)
|
||||
: undefined;
|
||||
const supportedReasoningEfforts =
|
||||
reasoningEffortValues && reasoningEffortValues.length > 0
|
||||
? buildSupportedReasoningEfforts(reasoningEffortValues)
|
||||
: undefined;
|
||||
const configSchema =
|
||||
reasoningEffortValues && defaultReasoningEffort
|
||||
? buildReasoningConfigSchema(reasoningEffortValues, defaultReasoningEffort)
|
||||
: undefined;
|
||||
let modelCapabilities = model.capabilities ? { ...model.capabilities } : undefined;
|
||||
|
||||
if (reasoningEffortValues) {
|
||||
modelCapabilities = modelCapabilities || {};
|
||||
Object.assign(modelCapabilities, {
|
||||
reasoning: true,
|
||||
thinking: true,
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
model: modelId,
|
||||
remote_model: displayName,
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
license: "proprietary",
|
||||
modelfile: `FROM ${modelId}`,
|
||||
parameters: "",
|
||||
template: "",
|
||||
details: {
|
||||
parent_model: model.root || actualModelId || "",
|
||||
format: "openai",
|
||||
family,
|
||||
families: [family],
|
||||
parameter_size: "unknown",
|
||||
quantization_level: "dynamic",
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
model_info: {
|
||||
"general.architecture": architecture,
|
||||
"general.basename": displayName,
|
||||
...(typeof model.context_length === "number" ? { context_length: model.context_length } : {}),
|
||||
...(typeof model.context_length === "number"
|
||||
? { [`${architecture}.context_length`]: model.context_length }
|
||||
: {}),
|
||||
...(typeof model.max_output_tokens === "number"
|
||||
? { max_output_tokens: model.max_output_tokens }
|
||||
: {}),
|
||||
...(Array.isArray(model.input_modalities)
|
||||
? { input_modalities: model.input_modalities }
|
||||
: {}),
|
||||
...(Array.isArray(model.output_modalities)
|
||||
? { output_modalities: model.output_modalities }
|
||||
: {}),
|
||||
...(Array.isArray(model.supported_endpoints)
|
||||
? { supported_endpoints: model.supported_endpoints }
|
||||
: {}),
|
||||
...(modelCapabilities ? { capabilities: modelCapabilities } : {}),
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
capabilities: buildCapabilities(model),
|
||||
};
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return handleCorsOptions();
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params?: Promise<{ token: string }> | { token: string } } = {}
|
||||
) {
|
||||
const resolvedParams = params ? await params : undefined;
|
||||
const authorizedRequest = withPathTokenApiKey(request, resolvedParams?.token);
|
||||
const payload = await request
|
||||
.clone()
|
||||
.json()
|
||||
.catch(() => null);
|
||||
const requestedName = getRequestedModelName(payload);
|
||||
|
||||
if (!requestedName) {
|
||||
return Response.json(
|
||||
{
|
||||
error: "Model name is required",
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const catalogResponse = await getUnifiedModelsResponse(authorizedRequest, {
|
||||
"Content-Type": "application/json",
|
||||
...CORS_HEADERS,
|
||||
});
|
||||
const catalogBody = (await catalogResponse.json()) as { data?: OpenAiCatalogModel[] };
|
||||
|
||||
if (!catalogResponse.ok) {
|
||||
return Response.json(catalogBody, {
|
||||
status: catalogResponse.status,
|
||||
headers: {
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const expandedModels = Array.isArray(catalogBody.data)
|
||||
? expandVscodeServiceTierModels(catalogBody.data.filter(isUsableChatModel))
|
||||
: [];
|
||||
|
||||
const model = Array.isArray(expandedModels)
|
||||
? expandedModels.find((entry) => matchesRequestedModel(entry, requestedName))
|
||||
: undefined;
|
||||
|
||||
if (!model) {
|
||||
return Response.json(
|
||||
{
|
||||
error: `Model not found: ${requestedName}`,
|
||||
},
|
||||
{
|
||||
status: 404,
|
||||
headers: {
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return Response.json(buildShowPayload(model, requestedName), {
|
||||
headers: {
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
});
|
||||
}
|
||||
257
src/app/api/v1/vscode/[token]/api/tags/route.ts
Normal file
257
src/app/api/v1/vscode/[token]/api/tags/route.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import { getUnifiedModelsResponse } from "@/app/api/v1/models/catalog";
|
||||
import { getProviderConnections } from "@/lib/db/providers";
|
||||
import { getCanonicalModelMetadata } from "@/lib/modelMetadataRegistry";
|
||||
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
|
||||
import {
|
||||
buildReasoningConfigSchema,
|
||||
buildSupportedReasoningEfforts,
|
||||
getDefaultReasoningEffort,
|
||||
getReasoningVariantBaseModelId,
|
||||
getReasoningEffortValues,
|
||||
inferSelectedReasoningEffort,
|
||||
type VscodeCatalogModel,
|
||||
} from "@/app/api/v1/vscode/[token]/reasoningMetadata";
|
||||
import { getVscodeModelGroupingKey } from "@/app/api/v1/vscode/[token]/modelPresentation";
|
||||
import {
|
||||
expandVscodeServiceTierModels,
|
||||
getVscodeServiceTierVariantModelId,
|
||||
parseVscodeServiceTierVariantModelId,
|
||||
} from "@/app/api/v1/vscode/[token]/serviceTierVariants";
|
||||
import { getFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/[token]/familyFirstModelIds";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/[token]/tokenizedRequest";
|
||||
|
||||
type OpenAiCatalogModel = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
root?: string;
|
||||
parent?: string | null;
|
||||
owned_by?: string;
|
||||
type?: string;
|
||||
api_format?: string;
|
||||
context_length?: number;
|
||||
output_modalities?: string[];
|
||||
supported_endpoints?: string[];
|
||||
};
|
||||
|
||||
function getModelName(model: OpenAiCatalogModel) {
|
||||
return model.id || model.name || model.root || "";
|
||||
}
|
||||
|
||||
function isCodexOwnedModel(model: OpenAiCatalogModel) {
|
||||
const owner = typeof model.owned_by === "string" ? model.owned_by.trim().toLowerCase() : "";
|
||||
const modelName = getModelName(model).toLowerCase();
|
||||
|
||||
return owner === "codex" || modelName.startsWith("cx/") || modelName.startsWith("codex/");
|
||||
}
|
||||
|
||||
async function selectPreferredModels(models: OpenAiCatalogModel[]) {
|
||||
const activeConnections = (await getProviderConnections({ isActive: true })) as Array<{
|
||||
provider?: string;
|
||||
}>;
|
||||
const activeProviders = new Set(
|
||||
activeConnections
|
||||
.map((connection) =>
|
||||
typeof connection.provider === "string" ? connection.provider.trim().toLowerCase() : ""
|
||||
)
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
const preferCodexOnly =
|
||||
activeProviders.size > 0 && Array.from(activeProviders).every((provider) => provider === "codex");
|
||||
if (!preferCodexOnly) return models;
|
||||
|
||||
const codexModels = models.filter(isCodexOwnedModel);
|
||||
return codexModels.length > 0 ? codexModels : models;
|
||||
}
|
||||
|
||||
function isUsableChatModel(model: OpenAiCatalogModel) {
|
||||
if (typeof model.owned_by === "string" && model.owned_by.trim().toLowerCase() === "combo") {
|
||||
return false;
|
||||
}
|
||||
if (typeof model.parent === "string" && model.parent.length > 0) return false;
|
||||
if (typeof model.type === "string" && model.type !== "chat") return false;
|
||||
|
||||
const apiFormat = typeof model.api_format === "string" ? model.api_format : "chat-completions";
|
||||
if (apiFormat !== "chat-completions") return false;
|
||||
|
||||
if (
|
||||
Array.isArray(model.supported_endpoints) &&
|
||||
model.supported_endpoints.length > 0 &&
|
||||
!model.supported_endpoints.includes("chat")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(model.output_modalities) &&
|
||||
model.output_modalities.length > 0 &&
|
||||
!model.output_modalities.includes("text")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getOllamaModelFamily(model: OpenAiCatalogModel, canonicalFamily?: string | null) {
|
||||
const rawModelId = getModelName(model).trim();
|
||||
const tierParsedModel = parseVscodeServiceTierVariantModelId(rawModelId);
|
||||
const baseModelId = getReasoningVariantBaseModelId(tierParsedModel.baseModelId);
|
||||
const modelFamily = baseModelId.includes("/") ? baseModelId.split("/").slice(1).join("/") : baseModelId;
|
||||
|
||||
if (modelFamily) {
|
||||
return modelFamily;
|
||||
}
|
||||
|
||||
if (canonicalFamily && canonicalFamily.trim().length > 0) {
|
||||
return canonicalFamily.trim();
|
||||
}
|
||||
|
||||
return typeof model.owned_by === "string" && model.owned_by.trim().length > 0
|
||||
? model.owned_by.trim()
|
||||
: "omniroute";
|
||||
}
|
||||
|
||||
function toOllamaTagModel(model: OpenAiCatalogModel) {
|
||||
const actualModelId = model.id || model.root || "unknown";
|
||||
const canonicalMetadata = getCanonicalModelMetadata({
|
||||
provider: model.owned_by || null,
|
||||
model: model.root || model.id || model.name || null,
|
||||
});
|
||||
const family = getOllamaModelFamily(model, canonicalMetadata?.metadata.family || null);
|
||||
const modelId = getFamilyFirstPublishedModelId(actualModelId, family);
|
||||
const contextLength = typeof model.context_length === "number" ? model.context_length : 0;
|
||||
const reasoningEffortValues = getReasoningEffortValues(model as VscodeCatalogModel);
|
||||
const selectedReasoningEffort = reasoningEffortValues
|
||||
? inferSelectedReasoningEffort(model as VscodeCatalogModel, reasoningEffortValues) || "none"
|
||||
: undefined;
|
||||
const defaultReasoningEffort = reasoningEffortValues
|
||||
? getDefaultReasoningEffort(model as VscodeCatalogModel, reasoningEffortValues)
|
||||
: undefined;
|
||||
const supportedReasoningEfforts =
|
||||
reasoningEffortValues && reasoningEffortValues.length > 0
|
||||
? buildSupportedReasoningEfforts(reasoningEffortValues)
|
||||
: undefined;
|
||||
const configSchema =
|
||||
reasoningEffortValues && defaultReasoningEffort
|
||||
? buildReasoningConfigSchema(reasoningEffortValues, defaultReasoningEffort)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
name: modelId,
|
||||
model: modelId,
|
||||
modified_at: "2026-01-01T00:00:00Z",
|
||||
size: 0,
|
||||
digest: `omniroute:${modelId}`,
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
details: {
|
||||
format: "openai",
|
||||
family,
|
||||
parameter_size: contextLength > 0 ? `${contextLength} ctx` : "unknown",
|
||||
quantization_level: "dynamic",
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function filterCanonicalTagModels(models: OpenAiCatalogModel[]) {
|
||||
const allModelIds = new Set(models.map((model) => (model.id || model.root || model.name || "").trim()).filter(Boolean));
|
||||
const groupedModels = new Map<string, OpenAiCatalogModel>();
|
||||
const orderedGroupKeys: string[] = [];
|
||||
|
||||
for (const model of models) {
|
||||
const modelId = (model.id || model.root || model.name || "").trim();
|
||||
if (!modelId) continue;
|
||||
|
||||
const tierParsedModel = parseVscodeServiceTierVariantModelId(modelId);
|
||||
const baseModelId = getReasoningVariantBaseModelId(tierParsedModel.baseModelId);
|
||||
const canonicalModelId = tierParsedModel.serviceTier
|
||||
? getVscodeServiceTierVariantModelId(baseModelId, tierParsedModel.serviceTier)
|
||||
: baseModelId;
|
||||
if (canonicalModelId !== modelId && allModelIds.has(canonicalModelId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const groupKey = tierParsedModel.serviceTier
|
||||
? canonicalModelId
|
||||
: getVscodeModelGroupingKey(model) || canonicalModelId;
|
||||
const current = groupedModels.get(groupKey);
|
||||
if (!current) {
|
||||
groupedModels.set(groupKey, model);
|
||||
orderedGroupKeys.push(groupKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentId = (current.id || current.root || current.name || "").trim();
|
||||
if (currentId !== groupKey && modelId === canonicalModelId) {
|
||||
groupedModels.set(groupKey, model);
|
||||
}
|
||||
}
|
||||
|
||||
return orderedGroupKeys.map((groupKey) => groupedModels.get(groupKey)).filter(Boolean) as OpenAiCatalogModel[];
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return handleCorsOptions();
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params?: Promise<{ token: string }> | { token: string } } = {}
|
||||
) {
|
||||
const resolvedParams = params ? await params : undefined;
|
||||
const authorizedRequest = withPathTokenApiKey(request, resolvedParams?.token);
|
||||
const response = await getUnifiedModelsResponse(authorizedRequest, {
|
||||
"Content-Type": "application/json",
|
||||
...CORS_HEADERS,
|
||||
});
|
||||
const body = (await response.json()) as { data?: OpenAiCatalogModel[] };
|
||||
|
||||
if (!response.ok) {
|
||||
return Response.json(body, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const usableModels = Array.isArray(body.data) ? body.data.filter(isUsableChatModel) : [];
|
||||
const preferredModels = filterCanonicalTagModels(
|
||||
expandVscodeServiceTierModels(await selectPreferredModels(usableModels))
|
||||
);
|
||||
const models = preferredModels.map(toOllamaTagModel);
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
models,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
20
src/app/api/v1/vscode/[token]/api/version/route.ts
Normal file
20
src/app/api/v1/vscode/[token]/api/version/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
|
||||
|
||||
const OLLAMA_COMPAT_VERSION = "0.6.4";
|
||||
|
||||
export async function OPTIONS() {
|
||||
return handleCorsOptions();
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return Response.json(
|
||||
{
|
||||
version: OLLAMA_COMPAT_VERSION,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
10
src/app/api/v1/vscode/[token]/chat/completions/route.ts
Normal file
10
src/app/api/v1/vscode/[token]/chat/completions/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { POST as basePost, OPTIONS } from "@/app/api/v1/chat/completions/route";
|
||||
import { rewriteVscodeServiceTierRequest } from "@/app/api/v1/vscode/[token]/serviceTierVariants";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/[token]/tokenizedRequest";
|
||||
|
||||
export { OPTIONS };
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const authorizedRequest = withPathTokenApiKey(request);
|
||||
return basePost(await rewriteVscodeServiceTierRequest(authorizedRequest));
|
||||
}
|
||||
41
src/app/api/v1/vscode/[token]/combos/route.ts
Normal file
41
src/app/api/v1/vscode/[token]/combos/route.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* VS Code Combos endpoint — re-export base Ollama-compatible routes
|
||||
* and intercept GET to return combo metadata.
|
||||
*
|
||||
* The VS Code extension expects a standard Ollama server at the base URL,
|
||||
* so we re-export /api/version, /api/tags, etc. from the [token] parent route.
|
||||
*/
|
||||
import { getCombos } from "@/lib/db/combos";
|
||||
import { projectCombo, type PublicCombo } from "@/app/api/v1/combos/projectCombo";
|
||||
|
||||
// Re-export Ollama-compatible endpoints from the parent [token] route
|
||||
// so VS Code can validate the server version and list models normally
|
||||
export { OPTIONS } from "@/app/api/v1/vscode/[token]/route";
|
||||
export async function GET(request: Request) {
|
||||
// If client requests /api/version or other Ollama endpoints, delegate to parent
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname.includes("/api/version") || url.pathname.includes("/api/tags")) {
|
||||
const { GET: parentGET } = await import("@/app/api/v1/vscode/[token]/route");
|
||||
return parentGET(request);
|
||||
}
|
||||
|
||||
// Default: return combos metadata
|
||||
try {
|
||||
const combos = await getCombos();
|
||||
const data = (Array.isArray(combos) ? combos : [])
|
||||
.map((combo) => projectCombo(combo as Record<string, unknown>))
|
||||
.filter((combo): combo is PublicCombo => combo !== null);
|
||||
|
||||
return new Response(JSON.stringify({ object: "list", data, combos: data }), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: "Failed to fetch combos" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
85
src/app/api/v1/vscode/[token]/familyFirstModelIds.ts
Normal file
85
src/app/api/v1/vscode/[token]/familyFirstModelIds.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
const FAMILY_FIRST_MODEL_PATTERN = /^((?:gpt-[a-z0-9._-]+|claude[a-z0-9._-]*))(?:__provider_([a-z0-9-]+))(?:__tier_(priority|flex))?$/i;
|
||||
const TIER_SUFFIX_PATTERN = /(__tier_(?:priority|flex))$/i;
|
||||
|
||||
function normalizeFamily(value: string | null | undefined) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function splitActualModelId(modelId: string) {
|
||||
const trimmedModelId = modelId.trim();
|
||||
const slashIndex = trimmedModelId.indexOf("/");
|
||||
if (slashIndex <= 0 || slashIndex === trimmedModelId.length - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
providerPrefix: trimmedModelId.slice(0, slashIndex),
|
||||
providerModelId: trimmedModelId.slice(slashIndex + 1),
|
||||
};
|
||||
}
|
||||
|
||||
function extractTierSuffix(modelId: string) {
|
||||
const match = modelId.match(TIER_SUFFIX_PATTERN);
|
||||
return match?.[1] || "";
|
||||
}
|
||||
|
||||
function stripTierSuffix(modelId: string) {
|
||||
return modelId.replace(TIER_SUFFIX_PATTERN, "");
|
||||
}
|
||||
|
||||
function isFamilyFirstEligibleFamily(family: string) {
|
||||
const normalized = family.toLowerCase();
|
||||
return normalized.startsWith("gpt-") || normalized.startsWith("claude");
|
||||
}
|
||||
|
||||
export function getFamilyFirstPublishedModelId(actualModelId: string, family: string | null | undefined) {
|
||||
const normalizedFamily = normalizeFamily(family);
|
||||
if (!normalizedFamily || !isFamilyFirstEligibleFamily(normalizedFamily)) {
|
||||
return actualModelId;
|
||||
}
|
||||
|
||||
const parts = splitActualModelId(actualModelId);
|
||||
if (!parts) {
|
||||
return actualModelId;
|
||||
}
|
||||
|
||||
const tierSuffix = extractTierSuffix(parts.providerModelId);
|
||||
const providerModelBase = stripTierSuffix(parts.providerModelId);
|
||||
if (providerModelBase !== normalizedFamily) {
|
||||
return actualModelId;
|
||||
}
|
||||
|
||||
return `${normalizedFamily}__provider_${parts.providerPrefix}${tierSuffix}`;
|
||||
}
|
||||
|
||||
export function resolveFamilyFirstPublishedModelId(modelId: string | null | undefined) {
|
||||
const trimmedModelId = typeof modelId === "string" ? modelId.trim() : "";
|
||||
if (!trimmedModelId) {
|
||||
return trimmedModelId;
|
||||
}
|
||||
|
||||
const match = trimmedModelId.match(FAMILY_FIRST_MODEL_PATTERN);
|
||||
if (!match) {
|
||||
return trimmedModelId;
|
||||
}
|
||||
|
||||
const [, family, providerPrefix, serviceTier] = match;
|
||||
const tierSuffix = serviceTier ? `__tier_${serviceTier.toLowerCase()}` : "";
|
||||
return `${providerPrefix}/${family}${tierSuffix}`;
|
||||
}
|
||||
|
||||
export function getFamilyFirstModelCandidates(actualModelId: string, family: string | null | undefined) {
|
||||
const normalizedFamily = normalizeFamily(family);
|
||||
const candidates = new Set<string>([actualModelId]);
|
||||
const publishedModelId = getFamilyFirstPublishedModelId(actualModelId, normalizedFamily);
|
||||
if (publishedModelId !== actualModelId) {
|
||||
candidates.add(publishedModelId);
|
||||
}
|
||||
|
||||
if (normalizedFamily && isFamilyFirstEligibleFamily(normalizedFamily)) {
|
||||
const tierSuffix = extractTierSuffix(actualModelId);
|
||||
candidates.add(`${normalizedFamily}${tierSuffix}`);
|
||||
}
|
||||
|
||||
return [...candidates];
|
||||
}
|
||||
178
src/app/api/v1/vscode/[token]/modelPresentation.ts
Normal file
178
src/app/api/v1/vscode/[token]/modelPresentation.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { parseModel } from "@omniroute/open-sse/services/model";
|
||||
import {
|
||||
getCanonicalModelMetadata,
|
||||
type CanonicalModelMetadata,
|
||||
} from "@/lib/modelMetadataRegistry";
|
||||
import {
|
||||
getVscodeServiceTierVariantSuffix,
|
||||
parseVscodeServiceTierVariantModelId,
|
||||
supportsVscodeServiceTierVariants,
|
||||
} from "@/app/api/v1/vscode/[token]/serviceTierVariants";
|
||||
import { getReasoningVariantBaseModelId } from "@/app/api/v1/vscode/[token]/reasoningMetadata";
|
||||
import { resolveFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/[token]/familyFirstModelIds";
|
||||
type VscodeCatalogModel = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
root?: string;
|
||||
owned_by?: string;
|
||||
};
|
||||
|
||||
const PROVIDER_NAME_OVERRIDES: Record<string, string> = {
|
||||
codex: "Codex",
|
||||
cx: "Codex",
|
||||
github: "GitHub",
|
||||
gh: "GitHub",
|
||||
"gemini-cli": "Gemini",
|
||||
gemini: "Gemini",
|
||||
};
|
||||
|
||||
function tokenize(value: string) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.split(/[^a-z0-9]+/i)
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => part.length >= 4);
|
||||
}
|
||||
|
||||
function getProviderPrefix(metadata: CanonicalModelMetadata | null) {
|
||||
const providerKey = metadata?.providerAlias || metadata?.provider || "";
|
||||
if (providerKey && PROVIDER_NAME_OVERRIDES[providerKey]) {
|
||||
return PROVIDER_NAME_OVERRIDES[providerKey];
|
||||
}
|
||||
|
||||
const providerLabel = metadata?.providerLabel?.trim() || null;
|
||||
if (!providerLabel) {
|
||||
return null;
|
||||
}
|
||||
if (/codex/i.test(providerLabel)) {
|
||||
return "Codex";
|
||||
}
|
||||
if (/github/i.test(providerLabel)) {
|
||||
return "GitHub";
|
||||
}
|
||||
if (/gemini/i.test(providerLabel)) {
|
||||
return "Gemini";
|
||||
}
|
||||
|
||||
return providerLabel;
|
||||
}
|
||||
|
||||
function normalizeDisplayNameBranding(displayName: string) {
|
||||
return displayName
|
||||
.replace(/^OpenAI\s+Codex\b/i, "Codex")
|
||||
.replace(/^GitHub\s+Copilot\b/i, "GitHub")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function stripLeadingProviderPrefix(displayName: string, providerPrefix: string | null) {
|
||||
if (!providerPrefix) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
const escapedProviderPrefix = providerPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return displayName.replace(new RegExp(`^${escapedProviderPrefix}\\s+`, "i"), "").trim();
|
||||
}
|
||||
|
||||
function looksLikeTechnicalModelName(value: string) {
|
||||
return /\/|__provider_|__tier_|^[a-z0-9-]+\/[a-z0-9._-]+$/i.test(value);
|
||||
}
|
||||
|
||||
function humanizeModelIdentifier(modelId: string) {
|
||||
const identifier = (modelId.split("/").pop() || modelId).trim();
|
||||
if (!identifier) {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
return identifier
|
||||
.split(/[-_]+/)
|
||||
.filter(Boolean)
|
||||
.map(part => {
|
||||
if (/^gpt$/i.test(part)) return "GPT";
|
||||
if (/^[0-9]+(?:\.[0-9]+)*$/.test(part)) return part;
|
||||
if (/^[a-z][0-9]$/i.test(part)) return part.toUpperCase();
|
||||
return part.charAt(0).toUpperCase() + part.slice(1);
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function resolveFriendlyBaseDisplayName(
|
||||
rawModelId: string,
|
||||
metadata: CanonicalModelMetadata | null,
|
||||
fallbackValue: string
|
||||
) {
|
||||
const normalizedFallback = normalizeDisplayNameBranding(fallbackValue.trim());
|
||||
if (normalizedFallback && !looksLikeTechnicalModelName(normalizedFallback)) {
|
||||
return normalizedFallback;
|
||||
}
|
||||
|
||||
const normalizedModelId = resolveFamilyFirstPublishedModelId(rawModelId);
|
||||
const parsedTierModel = parseVscodeServiceTierVariantModelId(normalizedModelId);
|
||||
const canonicalBaseModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId);
|
||||
const parsed = parseModel(canonicalBaseModelId, "");
|
||||
const providerModelId =
|
||||
parsed.model ||
|
||||
(canonicalBaseModelId.includes("/")
|
||||
? canonicalBaseModelId.split("/").slice(1).join("/")
|
||||
: canonicalBaseModelId) ||
|
||||
fallbackValue;
|
||||
|
||||
return humanizeModelIdentifier(providerModelId);
|
||||
}
|
||||
|
||||
function prefixDisplayName(displayName: string, providerPrefix: string | null) {
|
||||
const normalizedProviderPrefix = providerPrefix?.trim() || null;
|
||||
const normalizedDisplayName = normalizeDisplayNameBranding(displayName);
|
||||
|
||||
if (!normalizedProviderPrefix) return normalizedDisplayName;
|
||||
|
||||
const providerTokens = tokenize(normalizedProviderPrefix);
|
||||
if (providerTokens.length === 0) return normalizedDisplayName;
|
||||
|
||||
const displayNameLower = normalizedDisplayName.toLowerCase();
|
||||
if (providerTokens.some((token) => displayNameLower.includes(token))) {
|
||||
return normalizedDisplayName;
|
||||
}
|
||||
|
||||
return `${normalizedProviderPrefix} ${stripLeadingProviderPrefix(normalizedDisplayName, normalizedProviderPrefix)}`.trim();
|
||||
}
|
||||
|
||||
export function resolveVscodeModelMetadata(model: VscodeCatalogModel) {
|
||||
const rawModelId = model.id || model.root || model.name || "";
|
||||
const normalizedModelId = resolveFamilyFirstPublishedModelId(rawModelId);
|
||||
const parsedTierModel = parseVscodeServiceTierVariantModelId(normalizedModelId);
|
||||
const canonicalBaseModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId);
|
||||
const parsed = parseModel(canonicalBaseModelId, "");
|
||||
const provider = parsed.provider || model.owned_by || undefined;
|
||||
const providerModel =
|
||||
parsed.model ||
|
||||
(canonicalBaseModelId.includes("/")
|
||||
? canonicalBaseModelId.split("/").slice(1).join("/")
|
||||
: canonicalBaseModelId) ||
|
||||
model.root ||
|
||||
model.id ||
|
||||
model.name ||
|
||||
undefined;
|
||||
|
||||
return providerModel && provider
|
||||
? getCanonicalModelMetadata({ provider, model: providerModel })
|
||||
: providerModel
|
||||
? getCanonicalModelMetadata({ model: providerModel })
|
||||
: null;
|
||||
}
|
||||
|
||||
export function getVscodeModelDisplayName(model: VscodeCatalogModel) {
|
||||
const rawModelId = model.id || model.root || model.name || "";
|
||||
const { serviceTier } = parseVscodeServiceTierVariantModelId(rawModelId);
|
||||
const metadata = resolveVscodeModelMetadata(model);
|
||||
const displayName = metadata?.displayName || model.name || model.id || model.root || "unknown";
|
||||
const prefixedDisplayName = prefixDisplayName(displayName, getProviderPrefix(metadata));
|
||||
const shouldShowTierSuffix = Boolean(serviceTier) || supportsVscodeServiceTierVariants(model);
|
||||
return shouldShowTierSuffix
|
||||
? `${prefixedDisplayName} (${getVscodeServiceTierVariantSuffix(serviceTier)})`
|
||||
: prefixedDisplayName;
|
||||
}
|
||||
|
||||
export function getVscodeModelGroupingKey(model: VscodeCatalogModel) {
|
||||
const metadata = resolveVscodeModelMetadata(model);
|
||||
return metadata?.qualifiedId || metadata?.model || model.id || model.name || model.root || "";
|
||||
}
|
||||
424
src/app/api/v1/vscode/[token]/models/route.ts
Normal file
424
src/app/api/v1/vscode/[token]/models/route.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
import { getUnifiedModelsResponse } from "@/app/api/v1/models/catalog";
|
||||
import { getResolvedModelCapabilities } from "@/lib/modelCapabilities";
|
||||
import { getCanonicalModelMetadata } from "@/lib/modelMetadataRegistry";
|
||||
import {
|
||||
buildReasoningConfigSchema,
|
||||
buildSupportedReasoningEfforts,
|
||||
getDefaultReasoningEffort,
|
||||
getCatalogModelName,
|
||||
formatReasoningEffortLabel,
|
||||
getReasoningEffortValues,
|
||||
getReasoningVariantBaseModelId,
|
||||
type VscodeCatalogModel,
|
||||
type VscodeModelConfigSchema,
|
||||
} from "@/app/api/v1/vscode/[token]/reasoningMetadata";
|
||||
import {
|
||||
getVscodeModelDisplayName,
|
||||
resolveVscodeModelMetadata,
|
||||
getVscodeModelGroupingKey,
|
||||
} from "@/app/api/v1/vscode/[token]/modelPresentation";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/[token]/tokenizedRequest";
|
||||
import {
|
||||
expandVscodeServiceTierModels,
|
||||
getVscodeServiceTierVariantSuffix,
|
||||
getVscodeServiceTierVariantModelId,
|
||||
parseVscodeServiceTierVariantModelId,
|
||||
} from "@/app/api/v1/vscode/[token]/serviceTierVariants";
|
||||
import { getFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/[token]/familyFirstModelIds";
|
||||
|
||||
type CatalogModelEntry = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
root?: string;
|
||||
owned_by?: string;
|
||||
parent?: string | null;
|
||||
type?: string;
|
||||
api_format?: string;
|
||||
context_length?: number;
|
||||
max_input_tokens?: number;
|
||||
max_output_tokens?: number;
|
||||
supported_endpoints?: string[];
|
||||
output_modalities?: string[];
|
||||
capabilities?: Record<string, boolean>;
|
||||
};
|
||||
|
||||
type VscodeImportModel = CatalogModelEntry & {
|
||||
url?: string;
|
||||
toolCalling?: boolean;
|
||||
vision?: boolean;
|
||||
maxInputTokens?: number;
|
||||
maxOutputTokens?: number;
|
||||
family?: string;
|
||||
supportsReasoningEffort?: string[];
|
||||
supportedReasoningEfforts?: string[];
|
||||
defaultReasoningEffort?: string;
|
||||
configurationSchema?: VscodeModelConfigSchema;
|
||||
configSchema?: VscodeModelConfigSchema;
|
||||
};
|
||||
|
||||
type VscodeModelsCatalogResponse = {
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
body: { data?: CatalogModelEntry[]; [key: string]: unknown };
|
||||
};
|
||||
|
||||
const VSCODE_CATALOG_CACHE_HEADERS = {
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0",
|
||||
Pragma: "no-cache",
|
||||
Expires: "0",
|
||||
} as const;
|
||||
|
||||
type EnrichModelForVscodeOptions = {
|
||||
preserveNativeId?: boolean;
|
||||
};
|
||||
|
||||
function isUsableChatModel(model: CatalogModelEntry) {
|
||||
if (typeof model.owned_by === "string" && model.owned_by.trim().toLowerCase() === "combo") {
|
||||
return false;
|
||||
}
|
||||
if (typeof model.parent === "string" && model.parent.length > 0) return false;
|
||||
if (typeof model.type === "string" && model.type !== "chat") return false;
|
||||
if (typeof model.api_format === "string" && model.api_format !== "chat-completions") {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
Array.isArray(model.supported_endpoints) &&
|
||||
model.supported_endpoints.length > 0 &&
|
||||
!model.supported_endpoints.includes("chat")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
Array.isArray(model.output_modalities) &&
|
||||
model.output_modalities.length > 0 &&
|
||||
!model.output_modalities.includes("text")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getModelImportReasoningEffortValues(model: VscodeCatalogModel, reasoningEffortValues: string[]) {
|
||||
const providerId =
|
||||
(model.owned_by || "").trim() ||
|
||||
(model.id || model.name || model.root || "").split("/")[0] ||
|
||||
"";
|
||||
if (providerId === "github" || providerId === "gh") {
|
||||
return reasoningEffortValues.filter((value) => value !== "xhigh");
|
||||
}
|
||||
return reasoningEffortValues;
|
||||
}
|
||||
|
||||
function getVscodeImportFamily(model: CatalogModelEntry, canonicalFamily?: string | null) {
|
||||
const rawModelId = (model.root || model.id || model.name || "").trim();
|
||||
const tierParsedModel = parseVscodeServiceTierVariantModelId(rawModelId);
|
||||
const baseModelId = getReasoningVariantBaseModelId(tierParsedModel.baseModelId);
|
||||
const modelFamily = baseModelId.includes("/") ? baseModelId.split("/").slice(1).join("/") : baseModelId;
|
||||
|
||||
if (modelFamily) {
|
||||
return modelFamily;
|
||||
}
|
||||
|
||||
if (canonicalFamily && canonicalFamily.trim().length > 0) {
|
||||
return canonicalFamily.trim();
|
||||
}
|
||||
|
||||
return typeof model.owned_by === "string" && model.owned_by.trim().length > 0
|
||||
? model.owned_by.trim()
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function getVscodeRawModelDisplayName(model: CatalogModelEntry) {
|
||||
const actualModelId = (model.id || model.name || model.root || "").trim();
|
||||
const canonicalMetadata = resolveVscodeModelMetadata(model);
|
||||
const { baseModelId } = parseVscodeServiceTierVariantModelId(actualModelId);
|
||||
const displayBaseModelId = getReasoningVariantBaseModelId(baseModelId);
|
||||
const baseDisplayName = getVscodeModelDisplayName({
|
||||
...model,
|
||||
id: displayBaseModelId,
|
||||
name: displayBaseModelId,
|
||||
root: displayBaseModelId,
|
||||
}).replace(/\s+\(Default\)$/u, "");
|
||||
const providerKey = canonicalMetadata?.providerAlias || canonicalMetadata?.provider || "";
|
||||
const providerPrefix = providerKey === "codex" || providerKey === "cx"
|
||||
? "Codex"
|
||||
: providerKey === "github" || providerKey === "gh"
|
||||
? "GitHub"
|
||||
: canonicalMetadata?.providerLabel || null;
|
||||
const prefixedDisplayName = providerPrefix && !baseDisplayName.toLowerCase().includes(providerPrefix.toLowerCase())
|
||||
? `${providerPrefix} ${baseDisplayName}`.trim()
|
||||
: baseDisplayName;
|
||||
const { serviceTier } = parseVscodeServiceTierVariantModelId(actualModelId);
|
||||
const reasoningEffortValues = getReasoningEffortValues(model as VscodeCatalogModel);
|
||||
const selectedReasoningEffort = reasoningEffortValues
|
||||
? getCatalogModelName(model as VscodeCatalogModel).match(/-(xhigh|high|medium|low|none)$/i)?.[1]?.toLowerCase()
|
||||
: undefined;
|
||||
|
||||
const suffixes: string[] = [];
|
||||
if (selectedReasoningEffort && selectedReasoningEffort !== "none") {
|
||||
suffixes.push(formatReasoningEffortLabel(selectedReasoningEffort));
|
||||
}
|
||||
if (serviceTier) {
|
||||
suffixes.push(getVscodeServiceTierVariantSuffix(serviceTier));
|
||||
}
|
||||
if (suffixes.length === 0) {
|
||||
return prefixedDisplayName;
|
||||
}
|
||||
if (suffixes.length === 1) {
|
||||
return `${prefixedDisplayName} (${suffixes[0]})`;
|
||||
}
|
||||
const [first, ...rest] = suffixes;
|
||||
return `${prefixedDisplayName} (${first}) ${rest.map((suffix) => `(${suffix})`).join(" ")}`;
|
||||
}
|
||||
|
||||
export function enrichModelForVscode(
|
||||
model: CatalogModelEntry,
|
||||
request: Request,
|
||||
options: EnrichModelForVscodeOptions = {}
|
||||
): VscodeImportModel {
|
||||
if (!isUsableChatModel(model)) return model;
|
||||
|
||||
const requestUrl = new URL(request.url);
|
||||
const tokenBasePath = requestUrl.pathname.replace(/\/models(?:\/raw)?\/?$/, "");
|
||||
const tokenBaseUrl = `${requestUrl.origin}${tokenBasePath}`;
|
||||
const canonicalMetadata = getCanonicalModelMetadata({
|
||||
provider: model.owned_by || null,
|
||||
model: model.root || model.id || model.name || null,
|
||||
});
|
||||
const family = getVscodeImportFamily(model, canonicalMetadata?.metadata.family || null);
|
||||
const resolvedCapabilities = getResolvedModelCapabilities(model.id || model.name || "");
|
||||
const reasoningEffortValues =
|
||||
resolvedCapabilities.reasoning === true
|
||||
? getReasoningEffortValues(model as VscodeCatalogModel)
|
||||
: undefined;
|
||||
const modelImportReasoningEffortValues =
|
||||
reasoningEffortValues && reasoningEffortValues.length > 0
|
||||
? getModelImportReasoningEffortValues(model as VscodeCatalogModel, reasoningEffortValues)
|
||||
: undefined;
|
||||
const defaultReasoningEffort = reasoningEffortValues
|
||||
? getDefaultReasoningEffort(model as VscodeCatalogModel, reasoningEffortValues)
|
||||
: undefined;
|
||||
const supportedReasoningEfforts =
|
||||
reasoningEffortValues && reasoningEffortValues.length > 0
|
||||
? buildSupportedReasoningEfforts(reasoningEffortValues)
|
||||
: undefined;
|
||||
const configSchema =
|
||||
reasoningEffortValues && defaultReasoningEffort
|
||||
? buildReasoningConfigSchema(reasoningEffortValues, defaultReasoningEffort)
|
||||
: undefined;
|
||||
const actualModelId = (model.id || model.name || model.root || "").trim();
|
||||
const publishedModelId = getFamilyFirstPublishedModelId(actualModelId, family || null);
|
||||
const resolvedModelId = options.preserveNativeId ? actualModelId : publishedModelId;
|
||||
const presentationModel = {
|
||||
...model,
|
||||
...(resolvedModelId ? { id: resolvedModelId } : {}),
|
||||
};
|
||||
|
||||
// Raw route: return a bare provider-native entry — no VS Code presentation
|
||||
// fields (url/toolCalling/vision/maxInputTokens/family/reasoning). Those are
|
||||
// only meaningful for the grouped/family-first surfaces.
|
||||
if (options.preserveNativeId) {
|
||||
return {
|
||||
...presentationModel,
|
||||
name: getVscodeRawModelDisplayName(presentationModel),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...presentationModel,
|
||||
name: options.preserveNativeId
|
||||
? getVscodeRawModelDisplayName(presentationModel)
|
||||
: getVscodeModelDisplayName(presentationModel),
|
||||
url: reasoningEffortValues
|
||||
? `${tokenBaseUrl}/responses#models.ai.azure.com`
|
||||
: `${tokenBaseUrl}/chat/completions#models.ai.azure.com`,
|
||||
toolCalling: resolvedCapabilities.toolCalling === true,
|
||||
vision: resolvedCapabilities.supportsVision === true,
|
||||
maxInputTokens:
|
||||
model.max_input_tokens || resolvedCapabilities.maxInputTokens || model.context_length,
|
||||
maxOutputTokens: model.max_output_tokens || resolvedCapabilities.maxOutputTokens,
|
||||
...(family ? { family } : {}),
|
||||
...(modelImportReasoningEffortValues ? { supportsReasoningEffort: modelImportReasoningEffortValues } : {}),
|
||||
...(supportedReasoningEfforts ? { supportedReasoningEfforts } : {}),
|
||||
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function expandVscodeRawModels(models: CatalogModelEntry[]) {
|
||||
const normalizedModels = models.map((model) => {
|
||||
const rawModelId = (model.id || model.name || model.root || "").trim();
|
||||
if (!rawModelId) {
|
||||
return model;
|
||||
}
|
||||
|
||||
const tierParsedModel = parseVscodeServiceTierVariantModelId(rawModelId);
|
||||
const normalizedBaseModelId = getReasoningVariantBaseModelId(tierParsedModel.baseModelId);
|
||||
const normalizedModelId = tierParsedModel.serviceTier
|
||||
? getVscodeServiceTierVariantModelId(normalizedBaseModelId, tierParsedModel.serviceTier)
|
||||
: normalizedBaseModelId;
|
||||
|
||||
if (normalizedModelId === rawModelId) {
|
||||
return model;
|
||||
}
|
||||
|
||||
return {
|
||||
...model,
|
||||
...(model.id ? { id: normalizedModelId } : {}),
|
||||
...(model.name ? { name: normalizedModelId } : {}),
|
||||
...(model.root ? { root: normalizedModelId } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
const tierExpandedModels = expandVscodeServiceTierModels(normalizedModels);
|
||||
const expandedModels: CatalogModelEntry[] = [];
|
||||
|
||||
for (const model of tierExpandedModels) {
|
||||
expandedModels.push(model);
|
||||
|
||||
const reasoningEffortValues = getReasoningEffortValues(model as VscodeCatalogModel);
|
||||
if (!reasoningEffortValues || reasoningEffortValues.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawModelId = (model.id || model.name || model.root || "").trim();
|
||||
if (!rawModelId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsedTierModel = parseVscodeServiceTierVariantModelId(rawModelId);
|
||||
const baseReasoningModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId);
|
||||
|
||||
for (const reasoningEffort of reasoningEffortValues) {
|
||||
if (reasoningEffort === "none") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const reasoningBaseModelId = `${baseReasoningModelId}-${reasoningEffort}`;
|
||||
const reasoningVariantModelId = parsedTierModel.serviceTier
|
||||
? getVscodeServiceTierVariantModelId(reasoningBaseModelId, parsedTierModel.serviceTier)
|
||||
: reasoningBaseModelId;
|
||||
expandedModels.push({
|
||||
...model,
|
||||
...(model.id ? { id: reasoningVariantModelId } : {}),
|
||||
...(model.name ? { name: reasoningVariantModelId } : {}),
|
||||
...(model.root ? { root: reasoningVariantModelId } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueModels = new Map<string, CatalogModelEntry>();
|
||||
|
||||
for (const model of expandedModels) {
|
||||
const uniqueKey = (model.id || model.name || model.root || "").trim();
|
||||
if (!uniqueKey || uniqueModels.has(uniqueKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uniqueModels.set(uniqueKey, model);
|
||||
}
|
||||
|
||||
return Array.from(uniqueModels.values());
|
||||
}
|
||||
|
||||
export async function getVscodeModelsCatalogResponse(
|
||||
request: Request
|
||||
): Promise<VscodeModelsCatalogResponse> {
|
||||
const response = await getUnifiedModelsResponse(request);
|
||||
const body = (await response.json()) as { data?: CatalogModelEntry[] };
|
||||
return {
|
||||
status: response.status,
|
||||
headers: {
|
||||
...Object.fromEntries(response.headers.entries()),
|
||||
...VSCODE_CATALOG_CACHE_HEADERS,
|
||||
},
|
||||
body: {
|
||||
...body,
|
||||
data: Array.isArray(body.data) ? body.data.filter(isUsableChatModel) : body.data,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params?: Promise<{ token: string }> | { token: string } } = {}
|
||||
) {
|
||||
const resolvedParams = params ? await params : undefined;
|
||||
const authorizedRequest = withPathTokenApiKey(request, resolvedParams?.token);
|
||||
const catalog = await getVscodeModelsCatalogResponse(authorizedRequest);
|
||||
const body = catalog.body;
|
||||
|
||||
if (catalog.status < 200 || catalog.status >= 300 || !Array.isArray(body.data)) {
|
||||
return Response.json(body, {
|
||||
status: catalog.status,
|
||||
headers: catalog.headers,
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json(
|
||||
(() => {
|
||||
const expandedModels = expandVscodeServiceTierModels(body.data);
|
||||
const allModelIds = new Set(
|
||||
expandedModels.map((model) => (model.id || model.name || model.root || "").trim()).filter(Boolean)
|
||||
);
|
||||
const groupedModels = new Map<string, CatalogModelEntry>();
|
||||
const orderedGroupKeys: string[] = [];
|
||||
|
||||
for (const model of expandedModels) {
|
||||
const modelId = (model.id || model.name || model.root || "").trim();
|
||||
if (!modelId) continue;
|
||||
|
||||
const tierParsedModel = parseVscodeServiceTierVariantModelId(modelId);
|
||||
const baseModelId = getReasoningVariantBaseModelId(tierParsedModel.baseModelId);
|
||||
const canonicalModelId = tierParsedModel.serviceTier
|
||||
? getVscodeServiceTierVariantModelId(baseModelId, tierParsedModel.serviceTier)
|
||||
: baseModelId;
|
||||
if (canonicalModelId !== modelId && allModelIds.has(canonicalModelId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const groupKey =
|
||||
tierParsedModel.serviceTier
|
||||
? canonicalModelId
|
||||
: getVscodeModelGroupingKey({
|
||||
...model,
|
||||
...(canonicalModelId ? { id: canonicalModelId } : {}),
|
||||
}) || canonicalModelId;
|
||||
const current = groupedModels.get(groupKey);
|
||||
if (!current) {
|
||||
groupedModels.set(groupKey, model);
|
||||
orderedGroupKeys.push(groupKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentId = (current.id || current.name || current.root || "").trim();
|
||||
if (currentId !== groupKey && modelId === canonicalModelId) {
|
||||
groupedModels.set(groupKey, model);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...body,
|
||||
data: orderedGroupKeys
|
||||
.map((groupKey) => groupedModels.get(groupKey))
|
||||
.filter(Boolean)
|
||||
.map((model) => enrichModelForVscode(model as CatalogModelEntry, authorizedRequest)),
|
||||
};
|
||||
})(),
|
||||
{
|
||||
status: catalog.status,
|
||||
headers: catalog.headers,
|
||||
}
|
||||
);
|
||||
}
|
||||
175
src/app/api/v1/vscode/[token]/reasoningMetadata.ts
Normal file
175
src/app/api/v1/vscode/[token]/reasoningMetadata.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { parseModel } from "@omniroute/open-sse/services/model";
|
||||
import { supportsXHighEffort } from "@omniroute/open-sse/config/providerModels";
|
||||
import { stripVscodeServiceTierVariantModelId } from "@/app/api/v1/vscode/[token]/serviceTierVariants";
|
||||
|
||||
export type VscodeCatalogModel = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
root?: string;
|
||||
owned_by?: string;
|
||||
capabilities?: Record<string, boolean>;
|
||||
supportsReasoningEffort?: string[];
|
||||
supportedReasoningEfforts?: string[];
|
||||
supports_reasoning_effort?: string[];
|
||||
defaultReasoningEffort?: string;
|
||||
default_reasoning_effort?: string;
|
||||
};
|
||||
|
||||
const EFFORT_SUFFIX_PATTERN = /-(xhigh|high|medium|low|none)$/i;
|
||||
const DEFAULT_REASONING_EFFORT = "none";
|
||||
const KNOWN_REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh"]);
|
||||
|
||||
export type VscodeModelConfigSchema = {
|
||||
type: "object";
|
||||
properties: {
|
||||
reasoningEffort: {
|
||||
type: "string";
|
||||
title: string;
|
||||
description: string;
|
||||
default: string;
|
||||
enum: string[];
|
||||
enumLabels: string[];
|
||||
enumDescriptions: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export function getCatalogModelName(model: VscodeCatalogModel) {
|
||||
return stripVscodeServiceTierVariantModelId(model.id || model.name || model.root || "");
|
||||
}
|
||||
|
||||
function normalizeReasoningEffortValue(value: string) {
|
||||
const normalized = value.trim().toLowerCase().replace(/[_\s-]+/g, "");
|
||||
if (normalized === "xhigh") return "xhigh";
|
||||
if (KNOWN_REASONING_EFFORTS.has(normalized)) return normalized;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getNativeReasoningEffortValues(model: VscodeCatalogModel) {
|
||||
const candidates = [
|
||||
model.supportsReasoningEffort,
|
||||
model.supportedReasoningEfforts,
|
||||
model.supports_reasoning_effort,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (!Array.isArray(candidate) || candidate.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalized = Array.from(
|
||||
new Set(candidate.map(value => typeof value === "string" ? normalizeReasoningEffortValue(value) : undefined).filter(Boolean))
|
||||
) as string[];
|
||||
|
||||
if (normalized.length > 0) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isReasoningCapableModel(model: VscodeCatalogModel) {
|
||||
return (
|
||||
model.capabilities?.reasoning === true ||
|
||||
model.capabilities?.thinking === true ||
|
||||
(getNativeReasoningEffortValues(model)?.length || 0) > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function getReasoningEffortValues(model: VscodeCatalogModel) {
|
||||
const nativeReasoningEffortValues = getNativeReasoningEffortValues(model);
|
||||
if (nativeReasoningEffortValues && nativeReasoningEffortValues.length > 0) {
|
||||
return nativeReasoningEffortValues;
|
||||
}
|
||||
|
||||
if (!isReasoningCapableModel(model)) return undefined;
|
||||
|
||||
const modelId = getCatalogModelName(model);
|
||||
const parsed = parseModel(modelId, "");
|
||||
const providerId = parsed.provider || model.owned_by || "";
|
||||
const providerModelId = parsed.model || model.root || modelId.split("/").pop() || modelId;
|
||||
const values = ["none", "low", "medium", "high"];
|
||||
|
||||
if (providerId && providerModelId && supportsXHighEffort(providerId, providerModelId)) {
|
||||
values.push("xhigh");
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
export function formatReasoningEffortLabel(level: string) {
|
||||
if (level === "xhigh") return "XHigh";
|
||||
return level.charAt(0).toUpperCase() + level.slice(1);
|
||||
}
|
||||
|
||||
function describeReasoningEffort(level: string) {
|
||||
switch (level) {
|
||||
case "none":
|
||||
return "Disables extra reasoning effort.";
|
||||
case "low":
|
||||
return "Uses a light amount of reasoning.";
|
||||
case "medium":
|
||||
return "Uses a balanced amount of reasoning.";
|
||||
case "high":
|
||||
return "Uses an extended amount of reasoning.";
|
||||
case "xhigh":
|
||||
return "Uses the maximum available reasoning effort.";
|
||||
default:
|
||||
return `Uses ${formatReasoningEffortLabel(level)} reasoning effort.`;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildSupportedReasoningEfforts(
|
||||
supportedValues: string[]
|
||||
): string[] {
|
||||
return [...supportedValues];
|
||||
}
|
||||
|
||||
export function inferSelectedReasoningEffort(
|
||||
model: VscodeCatalogModel,
|
||||
supportedValues?: string[]
|
||||
) {
|
||||
const modelId = getCatalogModelName(model);
|
||||
const match = modelId.match(EFFORT_SUFFIX_PATTERN);
|
||||
if (!match) return undefined;
|
||||
|
||||
const selected = match[1]?.toLowerCase();
|
||||
if (!selected) return undefined;
|
||||
if (Array.isArray(supportedValues) && supportedValues.length > 0 && !supportedValues.includes(selected)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
export function getReasoningVariantBaseModelId(modelId: string) {
|
||||
return modelId.replace(EFFORT_SUFFIX_PATTERN, "");
|
||||
}
|
||||
|
||||
export function getDefaultReasoningEffort(
|
||||
model: VscodeCatalogModel,
|
||||
supportedValues?: string[]
|
||||
) {
|
||||
return inferSelectedReasoningEffort(model, supportedValues) || DEFAULT_REASONING_EFFORT;
|
||||
}
|
||||
|
||||
export function buildReasoningConfigSchema(
|
||||
supportedValues: string[],
|
||||
defaultReasoningEffort: string
|
||||
): VscodeModelConfigSchema {
|
||||
return {
|
||||
type: "object",
|
||||
properties: {
|
||||
reasoningEffort: {
|
||||
type: "string",
|
||||
title: "Reasoning effort",
|
||||
description: "Controls how much reasoning effort the model uses.",
|
||||
default: defaultReasoningEffort,
|
||||
enum: supportedValues,
|
||||
enumLabels: supportedValues.map(formatReasoningEffortLabel),
|
||||
enumDescriptions: supportedValues.map(describeReasoningEffort),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
10
src/app/api/v1/vscode/[token]/responses/route.ts
Normal file
10
src/app/api/v1/vscode/[token]/responses/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { POST as basePost, OPTIONS } from "@/app/api/v1/responses/route";
|
||||
import { rewriteVscodeServiceTierRequest } from "@/app/api/v1/vscode/[token]/serviceTierVariants";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/[token]/tokenizedRequest";
|
||||
|
||||
export { OPTIONS };
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const authorizedRequest = withPathTokenApiKey(request);
|
||||
return basePost(await rewriteVscodeServiceTierRequest(authorizedRequest));
|
||||
}
|
||||
20
src/app/api/v1/vscode/[token]/route.ts
Normal file
20
src/app/api/v1/vscode/[token]/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { CORS_HEADERS } from "@/shared/utils/cors";
|
||||
|
||||
type VscodeTokenParams = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, { headers: CORS_HEADERS });
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
context?: { params: Promise<VscodeTokenParams> | VscodeTokenParams }
|
||||
) {
|
||||
const modelsRoute = await import("@/app/api/v1/vscode/[token]/models/route");
|
||||
const requestUrl = new URL(request.url);
|
||||
requestUrl.pathname = `${requestUrl.pathname.replace(/\/+$/, "")}/models`;
|
||||
const modelsRequest = new Request(requestUrl, request);
|
||||
return modelsRoute.GET(modelsRequest, context);
|
||||
}
|
||||
190
src/app/api/v1/vscode/[token]/serviceTierVariants.ts
Normal file
190
src/app/api/v1/vscode/[token]/serviceTierVariants.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS } from "@/lib/providers/codexFastTier";
|
||||
import { normalizeServiceTierId, type ServiceTierId } from "@/shared/utils/serviceTierLabels";
|
||||
import { resolveFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/[token]/familyFirstModelIds";
|
||||
|
||||
const SERVICE_TIER_VARIANT_PATTERN = /__tier_(priority|flex)$/i;
|
||||
const SUPPORTED_VSCODE_SERVICE_TIERS: readonly ServiceTierId[] = ["priority", "flex"];
|
||||
|
||||
export type VscodeServiceTierModelLike = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
root?: string;
|
||||
owned_by?: string;
|
||||
};
|
||||
|
||||
export function parseVscodeServiceTierVariantModelId(modelId: string | null | undefined): {
|
||||
baseModelId: string;
|
||||
serviceTier?: ServiceTierId;
|
||||
} {
|
||||
const rawModelId = typeof modelId === "string" ? modelId.trim() : "";
|
||||
if (!rawModelId) {
|
||||
return { baseModelId: "" };
|
||||
}
|
||||
|
||||
const match = rawModelId.match(SERVICE_TIER_VARIANT_PATTERN);
|
||||
if (!match) {
|
||||
return { baseModelId: rawModelId };
|
||||
}
|
||||
|
||||
const baseModelId = rawModelId.replace(SERVICE_TIER_VARIANT_PATTERN, "");
|
||||
const serviceTier = normalizeServiceTierId(match[1]);
|
||||
return serviceTier === "standard" ? { baseModelId } : { baseModelId, serviceTier };
|
||||
}
|
||||
|
||||
export function stripVscodeServiceTierVariantModelId(modelId: string | null | undefined): string {
|
||||
return parseVscodeServiceTierVariantModelId(modelId).baseModelId;
|
||||
}
|
||||
|
||||
export function isVscodeServiceTierVariantModelId(modelId: string | null | undefined): boolean {
|
||||
return Boolean(parseVscodeServiceTierVariantModelId(modelId).serviceTier);
|
||||
}
|
||||
|
||||
export function getVscodeServiceTierVariantModelId(
|
||||
baseModelId: string,
|
||||
serviceTier: ServiceTierId
|
||||
): string {
|
||||
if (serviceTier === "standard") {
|
||||
return baseModelId;
|
||||
}
|
||||
return `${baseModelId}__tier_${serviceTier}`;
|
||||
}
|
||||
|
||||
function getRawModelId(model: VscodeServiceTierModelLike): string {
|
||||
return (model.id || model.name || model.root || "").trim();
|
||||
}
|
||||
|
||||
function getModelProvider(model: VscodeServiceTierModelLike, baseModelId: string): string {
|
||||
const owner = typeof model.owned_by === "string" ? model.owned_by.trim().toLowerCase() : "";
|
||||
if (owner) {
|
||||
return owner;
|
||||
}
|
||||
const prefix = baseModelId.split("/")[0]?.trim().toLowerCase() || "";
|
||||
return prefix;
|
||||
}
|
||||
|
||||
function supportsCodexServiceTierModel(baseModelId: string): boolean {
|
||||
const normalizedModel = (baseModelId.split("/").pop() || baseModelId).trim().toLowerCase();
|
||||
if (!normalizedModel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS.some((candidate) => {
|
||||
const normalizedCandidate = candidate.trim().toLowerCase();
|
||||
return normalizedModel === normalizedCandidate || normalizedModel.startsWith(normalizedCandidate);
|
||||
});
|
||||
}
|
||||
|
||||
export function supportsVscodeServiceTierVariants(model: VscodeServiceTierModelLike): boolean {
|
||||
const rawModelId = getRawModelId(model);
|
||||
if (!rawModelId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId);
|
||||
const provider = getModelProvider(model, baseModelId);
|
||||
if (provider !== "codex" && provider !== "cx") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return supportsCodexServiceTierModel(baseModelId);
|
||||
}
|
||||
|
||||
function cloneModelIdentifiers<T extends VscodeServiceTierModelLike>(
|
||||
model: T,
|
||||
modelId: string
|
||||
): T {
|
||||
return {
|
||||
...model,
|
||||
...(model.id ? { id: modelId } : {}),
|
||||
...(model.name ? { name: modelId } : {}),
|
||||
...(model.root ? { root: modelId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function expandVscodeServiceTierModels<T extends VscodeServiceTierModelLike>(models: T[]): T[] {
|
||||
const expanded: T[] = [];
|
||||
|
||||
for (const model of models) {
|
||||
const rawModelId = getRawModelId(model);
|
||||
if (!rawModelId) {
|
||||
expanded.push(model);
|
||||
continue;
|
||||
}
|
||||
|
||||
const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId);
|
||||
const baseModel = rawModelId === baseModelId ? model : cloneModelIdentifiers(model, baseModelId);
|
||||
expanded.push(baseModel as T);
|
||||
|
||||
if (!supportsVscodeServiceTierVariants(model)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const serviceTier of SUPPORTED_VSCODE_SERVICE_TIERS) {
|
||||
expanded.push(cloneModelIdentifiers(baseModel as T, getVscodeServiceTierVariantModelId(baseModelId, serviceTier)));
|
||||
}
|
||||
}
|
||||
|
||||
return expanded;
|
||||
}
|
||||
|
||||
export function getVscodeServiceTierVariantSuffix(serviceTier: ServiceTierId | undefined): string {
|
||||
if (serviceTier === "priority") {
|
||||
return "Fast";
|
||||
}
|
||||
if (serviceTier === "flex") {
|
||||
return "Flex";
|
||||
}
|
||||
return "Default";
|
||||
}
|
||||
|
||||
export function resolveVscodeServiceTierRequest(body: Record<string, unknown>): Record<string, unknown> {
|
||||
const rawModelId = typeof body.model === "string" ? body.model.trim() : "";
|
||||
if (!rawModelId) {
|
||||
return body;
|
||||
}
|
||||
|
||||
const resolvedModelId = resolveFamilyFirstPublishedModelId(rawModelId);
|
||||
|
||||
const { baseModelId, serviceTier } = parseVscodeServiceTierVariantModelId(resolvedModelId);
|
||||
if (!serviceTier) {
|
||||
if (resolvedModelId === rawModelId) {
|
||||
return body;
|
||||
}
|
||||
|
||||
return {
|
||||
...body,
|
||||
model: resolvedModelId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...body,
|
||||
model: baseModelId,
|
||||
...(body.service_tier === undefined ? { service_tier: serviceTier } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function rewriteVscodeServiceTierRequest(request: Request): Promise<Request> {
|
||||
if (request.method !== "POST") {
|
||||
return request;
|
||||
}
|
||||
|
||||
const body = await request.clone().json().catch(() => null);
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
||||
return request;
|
||||
}
|
||||
|
||||
const rewrittenBody = resolveVscodeServiceTierRequest(body as Record<string, unknown>);
|
||||
if (rewrittenBody === body) {
|
||||
return request;
|
||||
}
|
||||
|
||||
const headers = new Headers(request.headers);
|
||||
headers.delete("content-length");
|
||||
|
||||
return new Request(request.url, {
|
||||
method: request.method,
|
||||
headers,
|
||||
body: JSON.stringify(rewrittenBody),
|
||||
});
|
||||
}
|
||||
50
src/app/api/v1/vscode/[token]/tokenizedRequest.ts
Normal file
50
src/app/api/v1/vscode/[token]/tokenizedRequest.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
function inferTokenFromVscodePath(request: Request) {
|
||||
try {
|
||||
const url = new URL(request.url, "http://localhost");
|
||||
const segments = url.pathname
|
||||
.split("/")
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const vscodeIndex = segments.indexOf("vscode");
|
||||
if (vscodeIndex === -1) return null;
|
||||
|
||||
const candidate = segments[vscodeIndex + 1];
|
||||
if (!candidate || candidate === "raw" || candidate === "combos") {
|
||||
const nestedCandidate = segments[vscodeIndex + 2];
|
||||
return nestedCandidate ? decodeURIComponent(nestedCandidate) : null;
|
||||
}
|
||||
|
||||
return decodeURIComponent(candidate);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function withPathTokenApiKey(request: Request, token?: string) {
|
||||
const resolvedToken = token || inferTokenFromVscodePath(request);
|
||||
if (!resolvedToken) return request;
|
||||
|
||||
const headers = new Headers(request.headers);
|
||||
|
||||
if (!headers.has("x-api-key")) {
|
||||
headers.set("x-api-key", resolvedToken);
|
||||
}
|
||||
|
||||
if (!headers.has("authorization")) {
|
||||
headers.set("authorization", `Bearer ${resolvedToken}`);
|
||||
}
|
||||
|
||||
const method = request.method;
|
||||
const init: RequestInit & { duplex?: "half" } = {
|
||||
method,
|
||||
headers,
|
||||
};
|
||||
|
||||
if (method !== "GET" && method !== "HEAD") {
|
||||
init.body = request.body;
|
||||
init.duplex = "half";
|
||||
}
|
||||
|
||||
return new Request(request.url, init);
|
||||
}
|
||||
10
src/app/api/v1/vscode/[token]/v1/chat/completions/route.ts
Normal file
10
src/app/api/v1/vscode/[token]/v1/chat/completions/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { POST as basePost, OPTIONS } from "@/app/api/v1/chat/completions/route";
|
||||
import { rewriteVscodeServiceTierRequest } from "@/app/api/v1/vscode/[token]/serviceTierVariants";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/[token]/tokenizedRequest";
|
||||
|
||||
export { OPTIONS };
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const authorizedRequest = withPathTokenApiKey(request);
|
||||
return basePost(await rewriteVscodeServiceTierRequest(authorizedRequest));
|
||||
}
|
||||
1
src/app/api/v1/vscode/[token]/v1/models/route.ts
Normal file
1
src/app/api/v1/vscode/[token]/v1/models/route.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { GET, OPTIONS } from "@/app/api/v1/vscode/[token]/models/route";
|
||||
421
src/app/api/v1/vscode/combos/[token]/[[...slug]]/route.ts
Normal file
421
src/app/api/v1/vscode/combos/[token]/[[...slug]]/route.ts
Normal file
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* VS Code Combos endpoint with Ollama compatibility
|
||||
*
|
||||
* Intercepts both:
|
||||
* - GET /api/v1/vscode/combos/{token} → returns combo metadata
|
||||
* - GET /api/v1/vscode/combos/{token}/api/version → returns Ollama-compatible version
|
||||
* - GET /api/v1/vscode/combos/{token}/api/tags → exposes combo catalog in Ollama format
|
||||
*/
|
||||
import { getUnifiedModelsResponse } from "@/app/api/v1/models/catalog";
|
||||
import {
|
||||
buildReasoningConfigSchema,
|
||||
buildSupportedReasoningEfforts,
|
||||
getDefaultReasoningEffort,
|
||||
getReasoningEffortValues,
|
||||
inferSelectedReasoningEffort,
|
||||
type VscodeCatalogModel,
|
||||
} from "@/app/api/v1/vscode/[token]/reasoningMetadata";
|
||||
import {
|
||||
getVscodeRawModelDisplayName,
|
||||
} from "@/app/api/v1/vscode/[token]/models/route";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/[token]/tokenizedRequest";
|
||||
import { getCanonicalModelMetadata } from "@/lib/modelMetadataRegistry";
|
||||
import { CORS_HEADERS } from "@/shared/utils/cors";
|
||||
|
||||
const OLLAMA_COMPAT_VERSION = "0.6.4";
|
||||
|
||||
type ComboCatalogEntry = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
root?: string;
|
||||
owned_by?: string;
|
||||
parent?: string | null;
|
||||
type?: string;
|
||||
api_format?: string;
|
||||
context_length?: number;
|
||||
max_input_tokens?: number;
|
||||
max_output_tokens?: number;
|
||||
input_modalities?: string[];
|
||||
output_modalities?: string[];
|
||||
supported_endpoints?: string[];
|
||||
capabilities?: Record<string, boolean>;
|
||||
[ key: string ]: unknown;
|
||||
};
|
||||
|
||||
function isComboCatalogEntry(model: ComboCatalogEntry) {
|
||||
return typeof model.owned_by === "string" && model.owned_by.trim().toLowerCase() === "combo";
|
||||
}
|
||||
|
||||
async function buildComboCatalog(request: Request) {
|
||||
const response = await getUnifiedModelsResponse(request, {
|
||||
"Content-Type": "application/json",
|
||||
...CORS_HEADERS,
|
||||
});
|
||||
const body = (await response.json()) as { data?: ComboCatalogEntry[]; [key: string]: unknown };
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
status: response.status,
|
||||
headers: { ...CORS_HEADERS },
|
||||
body,
|
||||
data: [] as ComboCatalogEntry[],
|
||||
};
|
||||
}
|
||||
|
||||
const data = Array.isArray(body.data) ? body.data.filter(isComboCatalogEntry) : [];
|
||||
return {
|
||||
status: response.status,
|
||||
headers: { ...CORS_HEADERS },
|
||||
body,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function getComboFamily(model: ComboCatalogEntry, canonicalFamily?: string | null) {
|
||||
const rawModelId = (model.id || model.name || model.root || "").trim();
|
||||
if (rawModelId) return rawModelId;
|
||||
if (canonicalFamily && canonicalFamily.trim().length > 0) return canonicalFamily.trim();
|
||||
return "combo";
|
||||
}
|
||||
|
||||
function toOllamaTagCombo(combo: ComboCatalogEntry) {
|
||||
const actualModelId = (combo.id || combo.name || combo.root || "unknown").trim();
|
||||
const canonicalMetadata = getCanonicalModelMetadata({
|
||||
provider: combo.owned_by || null,
|
||||
model: combo.root || combo.id || combo.name || null,
|
||||
});
|
||||
const family = getComboFamily(combo, canonicalMetadata?.metadata.family || null);
|
||||
const contextLength = typeof combo.context_length === "number" ? combo.context_length : 0;
|
||||
const reasoningEffortValues = getReasoningEffortValues(combo as VscodeCatalogModel);
|
||||
const selectedReasoningEffort = reasoningEffortValues
|
||||
? inferSelectedReasoningEffort(combo as VscodeCatalogModel, reasoningEffortValues) || "none"
|
||||
: undefined;
|
||||
const defaultReasoningEffort = reasoningEffortValues
|
||||
? getDefaultReasoningEffort(combo as VscodeCatalogModel, reasoningEffortValues)
|
||||
: undefined;
|
||||
const supportedReasoningEfforts =
|
||||
reasoningEffortValues && reasoningEffortValues.length > 0
|
||||
? buildSupportedReasoningEfforts(reasoningEffortValues)
|
||||
: undefined;
|
||||
const configSchema =
|
||||
reasoningEffortValues && defaultReasoningEffort
|
||||
? buildReasoningConfigSchema(reasoningEffortValues, defaultReasoningEffort)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
name: actualModelId,
|
||||
model: actualModelId,
|
||||
modified_at: "2026-01-01T00:00:00Z",
|
||||
size: 0,
|
||||
digest: `omniroute:combo:${actualModelId}`,
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
details: {
|
||||
format: "omniroute-combo",
|
||||
family,
|
||||
parameter_size: contextLength > 0 ? `${contextLength} ctx` : "unknown",
|
||||
quantization_level: "dynamic",
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function buildComboShowPayload(combo: ComboCatalogEntry) {
|
||||
const actualModelId = (combo.id || combo.name || combo.root || "unknown").trim();
|
||||
const displayName = getVscodeRawModelDisplayName(combo);
|
||||
const canonicalMetadata = getCanonicalModelMetadata({
|
||||
provider: combo.owned_by || null,
|
||||
model: combo.root || combo.id || combo.name || null,
|
||||
});
|
||||
const family = getComboFamily(combo, canonicalMetadata?.metadata.family || null);
|
||||
const reasoningEffortValues = getReasoningEffortValues(combo as VscodeCatalogModel);
|
||||
const selectedReasoningEffort = reasoningEffortValues
|
||||
? inferSelectedReasoningEffort(combo as VscodeCatalogModel, reasoningEffortValues) || "none"
|
||||
: undefined;
|
||||
const defaultReasoningEffort = reasoningEffortValues
|
||||
? getDefaultReasoningEffort(combo as VscodeCatalogModel, reasoningEffortValues)
|
||||
: undefined;
|
||||
const supportedReasoningEfforts =
|
||||
reasoningEffortValues && reasoningEffortValues.length > 0
|
||||
? buildSupportedReasoningEfforts(reasoningEffortValues)
|
||||
: undefined;
|
||||
const configSchema =
|
||||
reasoningEffortValues && defaultReasoningEffort
|
||||
? buildReasoningConfigSchema(reasoningEffortValues, defaultReasoningEffort)
|
||||
: undefined;
|
||||
const modelCapabilities = combo.capabilities ? { ...combo.capabilities } : undefined;
|
||||
const capabilities = ["completion"];
|
||||
if (combo.capabilities?.vision) capabilities.push("vision");
|
||||
if (combo.capabilities?.tool_calling) capabilities.push("tools");
|
||||
if (combo.capabilities?.reasoning || combo.capabilities?.thinking) capabilities.push("thinking");
|
||||
|
||||
return {
|
||||
model: actualModelId,
|
||||
remote_model: displayName,
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
license: "proprietary",
|
||||
modelfile: `FROM ${actualModelId}`,
|
||||
parameters: "",
|
||||
template: "",
|
||||
details: {
|
||||
parent_model: combo.root || actualModelId,
|
||||
format: "omniroute-combo",
|
||||
family,
|
||||
families: [family],
|
||||
parameter_size:
|
||||
typeof combo.context_length === "number" ? `${combo.context_length} ctx` : "unknown",
|
||||
quantization_level: "dynamic",
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
model_info: {
|
||||
"general.architecture": "combo",
|
||||
"general.basename": displayName,
|
||||
...(typeof combo.context_length === "number" ? { context_length: combo.context_length } : {}),
|
||||
...(typeof combo.context_length === "number" ? { "combo.context_length": combo.context_length } : {}),
|
||||
...(typeof combo.max_input_tokens === "number"
|
||||
? { max_input_tokens: combo.max_input_tokens }
|
||||
: {}),
|
||||
...(typeof combo.max_output_tokens === "number"
|
||||
? { max_output_tokens: combo.max_output_tokens }
|
||||
: {}),
|
||||
...(Array.isArray(combo.input_modalities)
|
||||
? { input_modalities: combo.input_modalities }
|
||||
: {}),
|
||||
...(Array.isArray(combo.output_modalities)
|
||||
? { output_modalities: combo.output_modalities }
|
||||
: {}),
|
||||
...(modelCapabilities ? { capabilities: modelCapabilities } : {}),
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
capabilities,
|
||||
};
|
||||
}
|
||||
|
||||
function enrichComboForVscode(combo: ComboCatalogEntry, request: Request) {
|
||||
const requestUrl = new URL(request.url);
|
||||
const tokenBasePath = requestUrl.pathname.replace(/\/?$/, "");
|
||||
const tokenBaseUrl = `${requestUrl.origin}${tokenBasePath}`;
|
||||
const reasoningEffortValues = getReasoningEffortValues(combo as VscodeCatalogModel);
|
||||
const supportedReasoningEfforts =
|
||||
reasoningEffortValues && reasoningEffortValues.length > 0
|
||||
? buildSupportedReasoningEfforts(reasoningEffortValues)
|
||||
: undefined;
|
||||
const defaultReasoningEffort = reasoningEffortValues
|
||||
? getDefaultReasoningEffort(combo as VscodeCatalogModel, reasoningEffortValues)
|
||||
: undefined;
|
||||
const configSchema =
|
||||
reasoningEffortValues && defaultReasoningEffort
|
||||
? buildReasoningConfigSchema(reasoningEffortValues, defaultReasoningEffort)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...combo,
|
||||
name: getVscodeRawModelDisplayName(combo),
|
||||
url: reasoningEffortValues
|
||||
? `${tokenBaseUrl}/responses#models.ai.azure.com`
|
||||
: `${tokenBaseUrl}/chat/completions#models.ai.azure.com`,
|
||||
toolCalling: combo.capabilities?.tool_calling === true,
|
||||
vision: combo.capabilities?.vision === true,
|
||||
maxInputTokens: combo.max_input_tokens || combo.context_length,
|
||||
maxOutputTokens: combo.max_output_tokens,
|
||||
family: (combo.id || combo.name || combo.root || "combo").trim() || "combo",
|
||||
...(reasoningEffortValues ? { supportsReasoningEffort: reasoningEffortValues } : {}),
|
||||
...(supportedReasoningEfforts ? { supportedReasoningEfforts } : {}),
|
||||
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function readRequestedModelName(request: Request) {
|
||||
const payload = await request
|
||||
.clone()
|
||||
.json()
|
||||
.catch(() => null);
|
||||
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const record = payload as Record<string, unknown>;
|
||||
const candidate = record.name ?? record.model;
|
||||
return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : null;
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ token: string; slug?: string[] }> | { token: string; slug?: string[] };
|
||||
}
|
||||
) {
|
||||
const resolvedParams = await params;
|
||||
const slugPath = (resolvedParams.slug || []).join("/");
|
||||
const authorizedRequest = withPathTokenApiKey(request, resolvedParams.token);
|
||||
|
||||
// Handle /api/version request (Ollama compatibility check)
|
||||
if (slugPath === "api/version") {
|
||||
return Response.json(
|
||||
{ version: OLLAMA_COMPAT_VERSION },
|
||||
{ headers: { ...CORS_HEADERS } }
|
||||
);
|
||||
}
|
||||
|
||||
// Handle /api/tags request (redirect to models for compatibility)
|
||||
if (slugPath === "api/tags") {
|
||||
const catalog = await buildComboCatalog(authorizedRequest);
|
||||
if (catalog.status < 200 || catalog.status >= 300) {
|
||||
return Response.json(catalog.body, {
|
||||
status: catalog.status,
|
||||
headers: catalog.headers,
|
||||
});
|
||||
}
|
||||
return Response.json(
|
||||
{ models: catalog.data.map(toOllamaTagCombo) },
|
||||
{ headers: { ...CORS_HEADERS } }
|
||||
);
|
||||
}
|
||||
|
||||
// Default: return combos metadata
|
||||
try {
|
||||
const catalog = await buildComboCatalog(authorizedRequest);
|
||||
if (catalog.status < 200 || catalog.status >= 300) {
|
||||
return Response.json(catalog.body, {
|
||||
status: catalog.status,
|
||||
headers: catalog.headers,
|
||||
});
|
||||
}
|
||||
const data = catalog.data.map((combo) => enrichComboForVscode(combo, authorizedRequest));
|
||||
|
||||
return new Response(JSON.stringify({ object: "list", data }), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-store",
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: "Failed to fetch combos" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ token: string; slug?: string[] }> | { token: string; slug?: string[] };
|
||||
}
|
||||
) {
|
||||
const resolvedParams = await params;
|
||||
const slugPath = (resolvedParams.slug || []).join("/");
|
||||
const authorizedRequest = withPathTokenApiKey(request, resolvedParams.token);
|
||||
|
||||
if (slugPath !== "api/show") {
|
||||
return new Response(JSON.stringify({ error: "Not found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
|
||||
});
|
||||
}
|
||||
|
||||
const requestedName = await readRequestedModelName(request);
|
||||
if (!requestedName) {
|
||||
return Response.json(
|
||||
{ error: "Model name is required" },
|
||||
{ status: 400, headers: { ...CORS_HEADERS } }
|
||||
);
|
||||
}
|
||||
|
||||
const catalog = await buildComboCatalog(authorizedRequest);
|
||||
if (catalog.status < 200 || catalog.status >= 300) {
|
||||
return Response.json(catalog.body, {
|
||||
status: catalog.status,
|
||||
headers: catalog.headers,
|
||||
});
|
||||
}
|
||||
|
||||
const combo = catalog.data.find(
|
||||
(entry) => [entry.id, entry.name, entry.root].some((value) => value === requestedName)
|
||||
);
|
||||
|
||||
if (!combo) {
|
||||
return Response.json(
|
||||
{ error: `Model not found: ${requestedName}` },
|
||||
{ status: 404, headers: { ...CORS_HEADERS } }
|
||||
);
|
||||
}
|
||||
|
||||
return Response.json(buildComboShowPayload(combo), {
|
||||
headers: { ...CORS_HEADERS },
|
||||
});
|
||||
}
|
||||
5
src/app/api/v1/vscode/raw/[token]/api/chat/route.ts
Normal file
5
src/app/api/v1/vscode/raw/[token]/api/chat/route.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Alias Ollama compatível para chat no namespace raw.
|
||||
* Apenas reexporta a rota base dessa compatibilidade.
|
||||
*/
|
||||
export { POST, OPTIONS } from "@/app/api/v1/api/chat/route";
|
||||
327
src/app/api/v1/vscode/raw/[token]/api/show/route.ts
Normal file
327
src/app/api/v1/vscode/raw/[token]/api/show/route.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import { getUnifiedModelsResponse } from "@/app/api/v1/models/catalog";
|
||||
import { getVscodeRawModelDisplayName } from "@/app/api/v1/vscode/[token]/models/route";
|
||||
import { getCanonicalModelMetadata } from "@/lib/modelMetadataRegistry";
|
||||
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
|
||||
import { expandVscodeRawModels } from "@/app/api/v1/vscode/[token]/models/route";
|
||||
import {
|
||||
buildReasoningConfigSchema,
|
||||
buildSupportedReasoningEfforts,
|
||||
getDefaultReasoningEffort,
|
||||
getReasoningEffortValues,
|
||||
inferSelectedReasoningEffort,
|
||||
type VscodeCatalogModel,
|
||||
} from "@/app/api/v1/vscode/raw/[token]/reasoningMetadata";
|
||||
import { parseVscodeServiceTierVariantModelId } from "@/app/api/v1/vscode/raw/[token]/serviceTierVariants";
|
||||
import { getFamilyFirstModelCandidates } from "@/app/api/v1/vscode/raw/[token]/familyFirstModelIds";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/raw/[token]/tokenizedRequest";
|
||||
|
||||
type OpenAiCatalogModel = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
root?: string;
|
||||
parent?: string | null;
|
||||
owned_by?: string;
|
||||
type?: string;
|
||||
api_format?: string;
|
||||
context_length?: number;
|
||||
max_output_tokens?: number;
|
||||
capabilities?: Record<string, boolean>;
|
||||
input_modalities?: string[];
|
||||
output_modalities?: string[];
|
||||
supported_endpoints?: string[];
|
||||
};
|
||||
|
||||
function isUsableChatModel(model: OpenAiCatalogModel) {
|
||||
if (typeof model.owned_by === "string" && model.owned_by.trim().toLowerCase() === "combo") {
|
||||
return false;
|
||||
}
|
||||
if (typeof model.parent === "string" && model.parent.length > 0) return false;
|
||||
if (typeof model.type === "string" && model.type !== "chat") return false;
|
||||
|
||||
const apiFormat = typeof model.api_format === "string" ? model.api_format : "chat-completions";
|
||||
if (apiFormat !== "chat-completions") return false;
|
||||
|
||||
if (
|
||||
Array.isArray(model.supported_endpoints) &&
|
||||
model.supported_endpoints.length > 0 &&
|
||||
!model.supported_endpoints.includes("chat")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(model.output_modalities) &&
|
||||
model.output_modalities.length > 0 &&
|
||||
!model.output_modalities.includes("text")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getCatalogModelId(model: OpenAiCatalogModel) {
|
||||
return model.id || model.name || model.root || "unknown";
|
||||
}
|
||||
|
||||
function normalizeArchitectureKey(value: string) {
|
||||
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
||||
return normalized || "model";
|
||||
}
|
||||
|
||||
function normalizeArchitectureSource(value: string) {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === "cx") return "codex";
|
||||
if (normalized === "gh") return "github";
|
||||
return value;
|
||||
}
|
||||
|
||||
function getRequestedModelName(payload: unknown): string | null {
|
||||
if (!payload || typeof payload !== "object") return null;
|
||||
const record = payload as Record<string, unknown>;
|
||||
const candidate = record.name ?? record.model;
|
||||
return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : null;
|
||||
}
|
||||
|
||||
function getOllamaModelFamily(model: OpenAiCatalogModel, canonicalFamily?: string | null) {
|
||||
const rawModelId = getCatalogModelId(model).trim();
|
||||
const { baseModelId } = parseVscodeServiceTierVariantModelId(rawModelId);
|
||||
const modelFamily = baseModelId.includes("/") ? baseModelId.split("/").slice(1).join("/") : baseModelId;
|
||||
|
||||
if (modelFamily) {
|
||||
return modelFamily;
|
||||
}
|
||||
|
||||
if (canonicalFamily && canonicalFamily.trim().length > 0) {
|
||||
return canonicalFamily.trim();
|
||||
}
|
||||
|
||||
return typeof model.owned_by === "string" && model.owned_by.trim().length > 0
|
||||
? model.owned_by.trim()
|
||||
: "omniroute";
|
||||
}
|
||||
|
||||
function matchesRequestedModel(model: OpenAiCatalogModel, requestedName: string): boolean {
|
||||
const canonicalMetadata = getCanonicalModelMetadata({
|
||||
provider: model.owned_by || null,
|
||||
model: model.root || model.id || model.name || null,
|
||||
});
|
||||
const family = getOllamaModelFamily(model, canonicalMetadata?.metadata.family || null);
|
||||
const actualModelId = getCatalogModelId(model);
|
||||
|
||||
return [
|
||||
model.id,
|
||||
model.name,
|
||||
model.root,
|
||||
canonicalMetadata?.qualifiedId,
|
||||
canonicalMetadata?.model,
|
||||
...getFamilyFirstModelCandidates(actualModelId, family),
|
||||
].some((value) => value === requestedName);
|
||||
}
|
||||
|
||||
function buildCapabilities(model: OpenAiCatalogModel): string[] {
|
||||
const capabilities = ["completion"];
|
||||
if (model.capabilities?.vision) capabilities.push("vision");
|
||||
if (model.capabilities?.tool_calling) capabilities.push("tools");
|
||||
if (model.capabilities?.reasoning || model.capabilities?.thinking) capabilities.push("thinking");
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
function buildShowPayload(model: OpenAiCatalogModel, responseModelId?: string) {
|
||||
const actualModelId = getCatalogModelId(model);
|
||||
const displayName = getVscodeRawModelDisplayName(model);
|
||||
const canonicalMetadata = getCanonicalModelMetadata({
|
||||
provider: model.owned_by || null,
|
||||
model: model.root || model.id || model.name || null,
|
||||
});
|
||||
const family = getOllamaModelFamily(model, canonicalMetadata?.metadata.family || null);
|
||||
const modelId = responseModelId || actualModelId;
|
||||
const architectureSource =
|
||||
normalizeArchitectureSource(
|
||||
canonicalMetadata?.providerAlias || canonicalMetadata?.provider || model.owned_by || family || "model"
|
||||
);
|
||||
const architecture = normalizeArchitectureKey(
|
||||
architectureSource
|
||||
);
|
||||
const reasoningEffortValues = getReasoningEffortValues(model as VscodeCatalogModel);
|
||||
const selectedReasoningEffort = reasoningEffortValues
|
||||
? inferSelectedReasoningEffort(model as VscodeCatalogModel, reasoningEffortValues) || "none"
|
||||
: undefined;
|
||||
const defaultReasoningEffort = reasoningEffortValues
|
||||
? getDefaultReasoningEffort(model as VscodeCatalogModel, reasoningEffortValues)
|
||||
: undefined;
|
||||
const supportedReasoningEfforts =
|
||||
reasoningEffortValues && reasoningEffortValues.length > 0
|
||||
? buildSupportedReasoningEfforts(reasoningEffortValues)
|
||||
: undefined;
|
||||
const configSchema =
|
||||
reasoningEffortValues && defaultReasoningEffort
|
||||
? buildReasoningConfigSchema(reasoningEffortValues, defaultReasoningEffort)
|
||||
: undefined;
|
||||
let modelCapabilities = model.capabilities ? { ...model.capabilities } : undefined;
|
||||
|
||||
if (reasoningEffortValues) {
|
||||
modelCapabilities = modelCapabilities || {};
|
||||
Object.assign(modelCapabilities, {
|
||||
reasoning: true,
|
||||
thinking: true,
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
model: modelId,
|
||||
remote_model: displayName,
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
license: "proprietary",
|
||||
modelfile: `FROM ${modelId}`,
|
||||
parameters: "",
|
||||
template: "",
|
||||
details: {
|
||||
parent_model: model.root || actualModelId || "",
|
||||
format: "openai",
|
||||
family,
|
||||
families: [family],
|
||||
parameter_size: "unknown",
|
||||
quantization_level: "dynamic",
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
model_info: {
|
||||
"general.architecture": architecture,
|
||||
"general.basename": displayName,
|
||||
...(typeof model.context_length === "number" ? { context_length: model.context_length } : {}),
|
||||
...(typeof model.context_length === "number"
|
||||
? { [`${architecture}.context_length`]: model.context_length }
|
||||
: {}),
|
||||
...(typeof model.max_output_tokens === "number"
|
||||
? { max_output_tokens: model.max_output_tokens }
|
||||
: {}),
|
||||
...(Array.isArray(model.input_modalities)
|
||||
? { input_modalities: model.input_modalities }
|
||||
: {}),
|
||||
...(Array.isArray(model.output_modalities)
|
||||
? { output_modalities: model.output_modalities }
|
||||
: {}),
|
||||
...(Array.isArray(model.supported_endpoints)
|
||||
? { supported_endpoints: model.supported_endpoints }
|
||||
: {}),
|
||||
...(modelCapabilities ? { capabilities: modelCapabilities } : {}),
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
capabilities: buildCapabilities(model),
|
||||
};
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return handleCorsOptions();
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params?: Promise<{ token: string }> | { token: string } } = {}
|
||||
) {
|
||||
const resolvedParams = params ? await params : undefined;
|
||||
const authorizedRequest = withPathTokenApiKey(request, resolvedParams?.token);
|
||||
const payload = await request
|
||||
.clone()
|
||||
.json()
|
||||
.catch(() => null);
|
||||
const requestedName = getRequestedModelName(payload);
|
||||
|
||||
if (!requestedName) {
|
||||
return Response.json(
|
||||
{
|
||||
error: "Model name is required",
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const catalogResponse = await getUnifiedModelsResponse(authorizedRequest, {
|
||||
"Content-Type": "application/json",
|
||||
...CORS_HEADERS,
|
||||
});
|
||||
const catalogBody = (await catalogResponse.json()) as { data?: OpenAiCatalogModel[] };
|
||||
|
||||
if (!catalogResponse.ok) {
|
||||
return Response.json(catalogBody, {
|
||||
status: catalogResponse.status,
|
||||
headers: {
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const expandedModels = Array.isArray(catalogBody.data)
|
||||
? expandVscodeRawModels(catalogBody.data.filter(isUsableChatModel))
|
||||
: [];
|
||||
|
||||
const model = Array.isArray(expandedModels)
|
||||
? expandedModels.find((entry) => matchesRequestedModel(entry, requestedName))
|
||||
: undefined;
|
||||
|
||||
if (!model) {
|
||||
return Response.json(
|
||||
{
|
||||
error: `Model not found: ${requestedName}`,
|
||||
},
|
||||
{
|
||||
status: 404,
|
||||
headers: {
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return Response.json(buildShowPayload(model), {
|
||||
headers: {
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
});
|
||||
}
|
||||
213
src/app/api/v1/vscode/raw/[token]/api/tags/route.ts
Normal file
213
src/app/api/v1/vscode/raw/[token]/api/tags/route.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { getUnifiedModelsResponse } from "@/app/api/v1/models/catalog";
|
||||
import { getProviderConnections } from "@/lib/db/providers";
|
||||
import { getCanonicalModelMetadata } from "@/lib/modelMetadataRegistry";
|
||||
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
|
||||
import { expandVscodeRawModels } from "@/app/api/v1/vscode/[token]/models/route";
|
||||
import {
|
||||
buildReasoningConfigSchema,
|
||||
buildSupportedReasoningEfforts,
|
||||
getDefaultReasoningEffort,
|
||||
getReasoningVariantBaseModelId,
|
||||
getReasoningEffortValues,
|
||||
inferSelectedReasoningEffort,
|
||||
type VscodeCatalogModel,
|
||||
} from "@/app/api/v1/vscode/raw/[token]/reasoningMetadata";
|
||||
import { parseVscodeServiceTierVariantModelId } from "@/app/api/v1/vscode/raw/[token]/serviceTierVariants";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/raw/[token]/tokenizedRequest";
|
||||
|
||||
type OpenAiCatalogModel = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
root?: string;
|
||||
parent?: string | null;
|
||||
owned_by?: string;
|
||||
type?: string;
|
||||
api_format?: string;
|
||||
context_length?: number;
|
||||
output_modalities?: string[];
|
||||
supported_endpoints?: string[];
|
||||
};
|
||||
|
||||
function getModelName(model: OpenAiCatalogModel) {
|
||||
return model.id || model.name || model.root || "";
|
||||
}
|
||||
|
||||
function isCodexOwnedModel(model: OpenAiCatalogModel) {
|
||||
const owner = typeof model.owned_by === "string" ? model.owned_by.trim().toLowerCase() : "";
|
||||
const modelName = getModelName(model).toLowerCase();
|
||||
|
||||
return owner === "codex" || modelName.startsWith("cx/") || modelName.startsWith("codex/");
|
||||
}
|
||||
|
||||
async function selectPreferredModels(models: OpenAiCatalogModel[]) {
|
||||
const activeConnections = (await getProviderConnections({ isActive: true })) as Array<{
|
||||
provider?: string;
|
||||
}>;
|
||||
const activeProviders = new Set(
|
||||
activeConnections
|
||||
.map((connection) =>
|
||||
typeof connection.provider === "string" ? connection.provider.trim().toLowerCase() : ""
|
||||
)
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
const preferCodexOnly =
|
||||
activeProviders.size > 0 && Array.from(activeProviders).every((provider) => provider === "codex");
|
||||
if (!preferCodexOnly) return models;
|
||||
|
||||
const codexModels = models.filter(isCodexOwnedModel);
|
||||
return codexModels.length > 0 ? codexModels : models;
|
||||
}
|
||||
|
||||
function isUsableChatModel(model: OpenAiCatalogModel) {
|
||||
if (typeof model.owned_by === "string" && model.owned_by.trim().toLowerCase() === "combo") {
|
||||
return false;
|
||||
}
|
||||
if (typeof model.parent === "string" && model.parent.length > 0) return false;
|
||||
if (typeof model.type === "string" && model.type !== "chat") return false;
|
||||
|
||||
const apiFormat = typeof model.api_format === "string" ? model.api_format : "chat-completions";
|
||||
if (apiFormat !== "chat-completions") return false;
|
||||
|
||||
if (
|
||||
Array.isArray(model.supported_endpoints) &&
|
||||
model.supported_endpoints.length > 0 &&
|
||||
!model.supported_endpoints.includes("chat")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(model.output_modalities) &&
|
||||
model.output_modalities.length > 0 &&
|
||||
!model.output_modalities.includes("text")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getOllamaModelFamily(model: OpenAiCatalogModel, canonicalFamily?: string | null) {
|
||||
const rawModelId = getModelName(model).trim();
|
||||
const tierParsedModel = parseVscodeServiceTierVariantModelId(rawModelId);
|
||||
const baseModelId = getReasoningVariantBaseModelId(tierParsedModel.baseModelId);
|
||||
const modelFamily = baseModelId.includes("/") ? baseModelId.split("/").slice(1).join("/") : baseModelId;
|
||||
|
||||
if (modelFamily) {
|
||||
return modelFamily;
|
||||
}
|
||||
|
||||
if (canonicalFamily && canonicalFamily.trim().length > 0) {
|
||||
return canonicalFamily.trim();
|
||||
}
|
||||
|
||||
return typeof model.owned_by === "string" && model.owned_by.trim().length > 0
|
||||
? model.owned_by.trim()
|
||||
: "omniroute";
|
||||
}
|
||||
|
||||
function toOllamaTagModel(model: OpenAiCatalogModel) {
|
||||
const actualModelId = model.id || model.root || "unknown";
|
||||
const canonicalMetadata = getCanonicalModelMetadata({
|
||||
provider: model.owned_by || null,
|
||||
model: model.root || model.id || model.name || null,
|
||||
});
|
||||
const family = getOllamaModelFamily(model, canonicalMetadata?.metadata.family || null);
|
||||
const modelId = actualModelId;
|
||||
const contextLength = typeof model.context_length === "number" ? model.context_length : 0;
|
||||
const reasoningEffortValues = getReasoningEffortValues(model as VscodeCatalogModel);
|
||||
const selectedReasoningEffort = reasoningEffortValues
|
||||
? inferSelectedReasoningEffort(model as VscodeCatalogModel, reasoningEffortValues) || "none"
|
||||
: undefined;
|
||||
const defaultReasoningEffort = reasoningEffortValues
|
||||
? getDefaultReasoningEffort(model as VscodeCatalogModel, reasoningEffortValues)
|
||||
: undefined;
|
||||
const supportedReasoningEfforts =
|
||||
reasoningEffortValues && reasoningEffortValues.length > 0
|
||||
? buildSupportedReasoningEfforts(reasoningEffortValues)
|
||||
: undefined;
|
||||
const configSchema =
|
||||
reasoningEffortValues && defaultReasoningEffort
|
||||
? buildReasoningConfigSchema(reasoningEffortValues, defaultReasoningEffort)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
name: modelId,
|
||||
model: modelId,
|
||||
modified_at: "2026-01-01T00:00:00Z",
|
||||
size: 0,
|
||||
digest: `omniroute:${modelId}`,
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
details: {
|
||||
format: "openai",
|
||||
family,
|
||||
parameter_size: contextLength > 0 ? `${contextLength} ctx` : "unknown",
|
||||
quantization_level: "dynamic",
|
||||
...(reasoningEffortValues
|
||||
? {
|
||||
supports_reasoning_effort: reasoningEffortValues,
|
||||
supportsReasoningEffort: reasoningEffortValues,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
selected_reasoning_effort: selectedReasoningEffort,
|
||||
selectedReasoningEffort: selectedReasoningEffort,
|
||||
...(configSchema ? { configurationSchema: configSchema } : {}),
|
||||
...(configSchema ? { configSchema } : {}),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return handleCorsOptions();
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params?: Promise<{ token: string }> | { token: string } } = {}
|
||||
) {
|
||||
const resolvedParams = params ? await params : undefined;
|
||||
const authorizedRequest = withPathTokenApiKey(request, resolvedParams?.token);
|
||||
const response = await getUnifiedModelsResponse(authorizedRequest, {
|
||||
"Content-Type": "application/json",
|
||||
...CORS_HEADERS,
|
||||
});
|
||||
const body = (await response.json()) as { data?: OpenAiCatalogModel[] };
|
||||
|
||||
if (!response.ok) {
|
||||
return Response.json(body, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const usableModels = Array.isArray(body.data) ? body.data.filter(isUsableChatModel) : [];
|
||||
const preferredModels = expandVscodeRawModels(await selectPreferredModels(usableModels));
|
||||
const models = preferredModels.map(toOllamaTagModel);
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
models,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
20
src/app/api/v1/vscode/raw/[token]/api/version/route.ts
Normal file
20
src/app/api/v1/vscode/raw/[token]/api/version/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
|
||||
|
||||
const OLLAMA_COMPAT_VERSION = "0.6.4";
|
||||
|
||||
export async function OPTIONS() {
|
||||
return handleCorsOptions();
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return Response.json(
|
||||
{
|
||||
version: OLLAMA_COMPAT_VERSION,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
...CORS_HEADERS,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
10
src/app/api/v1/vscode/raw/[token]/chat/completions/route.ts
Normal file
10
src/app/api/v1/vscode/raw/[token]/chat/completions/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { POST as basePost, OPTIONS } from "@/app/api/v1/chat/completions/route";
|
||||
import { rewriteVscodeServiceTierRequest } from "@/app/api/v1/vscode/raw/[token]/serviceTierVariants";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/raw/[token]/tokenizedRequest";
|
||||
|
||||
export { OPTIONS };
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const authorizedRequest = withPathTokenApiKey(request);
|
||||
return basePost(await rewriteVscodeServiceTierRequest(authorizedRequest));
|
||||
}
|
||||
41
src/app/api/v1/vscode/raw/[token]/combos/route.ts
Normal file
41
src/app/api/v1/vscode/raw/[token]/combos/route.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* VS Code Combos endpoint — re-export base Ollama-compatible routes
|
||||
* and intercept GET to return combo metadata.
|
||||
*
|
||||
* The VS Code extension expects a standard Ollama server at the base URL,
|
||||
* so we re-export /api/version, /api/tags, etc. from the [token] parent route.
|
||||
*/
|
||||
import { getCombos } from "@/lib/db/combos";
|
||||
import { projectCombo, type PublicCombo } from "@/app/api/v1/combos/projectCombo";
|
||||
|
||||
// Re-export Ollama-compatible endpoints from the parent [token] route
|
||||
// so VS Code can validate the server version and list models normally
|
||||
export { OPTIONS } from "@/app/api/v1/vscode/raw/[token]/route";
|
||||
export async function GET(request: Request) {
|
||||
// If client requests /api/version or other Ollama endpoints, delegate to parent
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname.includes("/api/version") || url.pathname.includes("/api/tags")) {
|
||||
const { GET: parentGET } = await import("@/app/api/v1/vscode/raw/[token]/route");
|
||||
return parentGET(request);
|
||||
}
|
||||
|
||||
// Default: return combos metadata
|
||||
try {
|
||||
const combos = await getCombos();
|
||||
const data = (Array.isArray(combos) ? combos : [])
|
||||
.map((combo) => projectCombo(combo as Record<string, unknown>))
|
||||
.filter((combo): combo is PublicCombo => combo !== null);
|
||||
|
||||
return new Response(JSON.stringify({ object: "list", data, combos: data }), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: "Failed to fetch combos" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
85
src/app/api/v1/vscode/raw/[token]/familyFirstModelIds.ts
Normal file
85
src/app/api/v1/vscode/raw/[token]/familyFirstModelIds.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
const FAMILY_FIRST_MODEL_PATTERN = /^((?:gpt-[a-z0-9._-]+|claude[a-z0-9._-]*))(?:__provider_([a-z0-9-]+))(?:__tier_(priority|flex))?$/i;
|
||||
const TIER_SUFFIX_PATTERN = /(__tier_(?:priority|flex))$/i;
|
||||
|
||||
function normalizeFamily(value: string | null | undefined) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function splitActualModelId(modelId: string) {
|
||||
const trimmedModelId = modelId.trim();
|
||||
const slashIndex = trimmedModelId.indexOf("/");
|
||||
if (slashIndex <= 0 || slashIndex === trimmedModelId.length - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
providerPrefix: trimmedModelId.slice(0, slashIndex),
|
||||
providerModelId: trimmedModelId.slice(slashIndex + 1),
|
||||
};
|
||||
}
|
||||
|
||||
function extractTierSuffix(modelId: string) {
|
||||
const match = modelId.match(TIER_SUFFIX_PATTERN);
|
||||
return match?.[1] || "";
|
||||
}
|
||||
|
||||
function stripTierSuffix(modelId: string) {
|
||||
return modelId.replace(TIER_SUFFIX_PATTERN, "");
|
||||
}
|
||||
|
||||
function isFamilyFirstEligibleFamily(family: string) {
|
||||
const normalized = family.toLowerCase();
|
||||
return normalized.startsWith("gpt-") || normalized.startsWith("claude");
|
||||
}
|
||||
|
||||
export function getFamilyFirstPublishedModelId(actualModelId: string, family: string | null | undefined) {
|
||||
const normalizedFamily = normalizeFamily(family);
|
||||
if (!normalizedFamily || !isFamilyFirstEligibleFamily(normalizedFamily)) {
|
||||
return actualModelId;
|
||||
}
|
||||
|
||||
const parts = splitActualModelId(actualModelId);
|
||||
if (!parts) {
|
||||
return actualModelId;
|
||||
}
|
||||
|
||||
const tierSuffix = extractTierSuffix(parts.providerModelId);
|
||||
const providerModelBase = stripTierSuffix(parts.providerModelId);
|
||||
if (providerModelBase !== normalizedFamily) {
|
||||
return actualModelId;
|
||||
}
|
||||
|
||||
return `${normalizedFamily}__provider_${parts.providerPrefix}${tierSuffix}`;
|
||||
}
|
||||
|
||||
export function resolveFamilyFirstPublishedModelId(modelId: string | null | undefined) {
|
||||
const trimmedModelId = typeof modelId === "string" ? modelId.trim() : "";
|
||||
if (!trimmedModelId) {
|
||||
return trimmedModelId;
|
||||
}
|
||||
|
||||
const match = trimmedModelId.match(FAMILY_FIRST_MODEL_PATTERN);
|
||||
if (!match) {
|
||||
return trimmedModelId;
|
||||
}
|
||||
|
||||
const [, family, providerPrefix, serviceTier] = match;
|
||||
const tierSuffix = serviceTier ? `__tier_${serviceTier.toLowerCase()}` : "";
|
||||
return `${providerPrefix}/${family}${tierSuffix}`;
|
||||
}
|
||||
|
||||
export function getFamilyFirstModelCandidates(actualModelId: string, family: string | null | undefined) {
|
||||
const normalizedFamily = normalizeFamily(family);
|
||||
const candidates = new Set<string>([actualModelId]);
|
||||
const publishedModelId = getFamilyFirstPublishedModelId(actualModelId, normalizedFamily);
|
||||
if (publishedModelId !== actualModelId) {
|
||||
candidates.add(publishedModelId);
|
||||
}
|
||||
|
||||
if (normalizedFamily && isFamilyFirstEligibleFamily(normalizedFamily)) {
|
||||
const tierSuffix = extractTierSuffix(actualModelId);
|
||||
candidates.add(`${normalizedFamily}${tierSuffix}`);
|
||||
}
|
||||
|
||||
return [...candidates];
|
||||
}
|
||||
178
src/app/api/v1/vscode/raw/[token]/modelPresentation.ts
Normal file
178
src/app/api/v1/vscode/raw/[token]/modelPresentation.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { parseModel } from "@omniroute/open-sse/services/model";
|
||||
import {
|
||||
getCanonicalModelMetadata,
|
||||
type CanonicalModelMetadata,
|
||||
} from "@/lib/modelMetadataRegistry";
|
||||
import {
|
||||
getVscodeServiceTierVariantSuffix,
|
||||
parseVscodeServiceTierVariantModelId,
|
||||
supportsVscodeServiceTierVariants,
|
||||
} from "@/app/api/v1/vscode/raw/[token]/serviceTierVariants";
|
||||
import { getReasoningVariantBaseModelId } from "@/app/api/v1/vscode/raw/[token]/reasoningMetadata";
|
||||
import { resolveFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/raw/[token]/familyFirstModelIds";
|
||||
type VscodeCatalogModel = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
root?: string;
|
||||
owned_by?: string;
|
||||
};
|
||||
|
||||
const PROVIDER_NAME_OVERRIDES: Record<string, string> = {
|
||||
codex: "Codex",
|
||||
cx: "Codex",
|
||||
github: "GitHub",
|
||||
gh: "GitHub",
|
||||
"gemini-cli": "Gemini",
|
||||
gemini: "Gemini",
|
||||
};
|
||||
|
||||
function tokenize(value: string) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.split(/[^a-z0-9]+/i)
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => part.length >= 4);
|
||||
}
|
||||
|
||||
function getProviderPrefix(metadata: CanonicalModelMetadata | null) {
|
||||
const providerKey = metadata?.providerAlias || metadata?.provider || "";
|
||||
if (providerKey && PROVIDER_NAME_OVERRIDES[providerKey]) {
|
||||
return PROVIDER_NAME_OVERRIDES[providerKey];
|
||||
}
|
||||
|
||||
const providerLabel = metadata?.providerLabel?.trim() || null;
|
||||
if (!providerLabel) {
|
||||
return null;
|
||||
}
|
||||
if (/codex/i.test(providerLabel)) {
|
||||
return "Codex";
|
||||
}
|
||||
if (/github/i.test(providerLabel)) {
|
||||
return "GitHub";
|
||||
}
|
||||
if (/gemini/i.test(providerLabel)) {
|
||||
return "Gemini";
|
||||
}
|
||||
|
||||
return providerLabel;
|
||||
}
|
||||
|
||||
function normalizeDisplayNameBranding(displayName: string) {
|
||||
return displayName
|
||||
.replace(/^OpenAI\s+Codex\b/i, "Codex")
|
||||
.replace(/^GitHub\s+Copilot\b/i, "GitHub")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function stripLeadingProviderPrefix(displayName: string, providerPrefix: string | null) {
|
||||
if (!providerPrefix) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
const escapedProviderPrefix = providerPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return displayName.replace(new RegExp(`^${escapedProviderPrefix}\\s+`, "i"), "").trim();
|
||||
}
|
||||
|
||||
function looksLikeTechnicalModelName(value: string) {
|
||||
return /\/|__provider_|__tier_|^[a-z0-9-]+\/[a-z0-9._-]+$/i.test(value);
|
||||
}
|
||||
|
||||
function humanizeModelIdentifier(modelId: string) {
|
||||
const identifier = (modelId.split("/").pop() || modelId).trim();
|
||||
if (!identifier) {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
return identifier
|
||||
.split(/[-_]+/)
|
||||
.filter(Boolean)
|
||||
.map(part => {
|
||||
if (/^gpt$/i.test(part)) return "GPT";
|
||||
if (/^[0-9]+(?:\.[0-9]+)*$/.test(part)) return part;
|
||||
if (/^[a-z][0-9]$/i.test(part)) return part.toUpperCase();
|
||||
return part.charAt(0).toUpperCase() + part.slice(1);
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function resolveFriendlyBaseDisplayName(
|
||||
rawModelId: string,
|
||||
metadata: CanonicalModelMetadata | null,
|
||||
fallbackValue: string
|
||||
) {
|
||||
const normalizedFallback = normalizeDisplayNameBranding(fallbackValue.trim());
|
||||
if (normalizedFallback && !looksLikeTechnicalModelName(normalizedFallback)) {
|
||||
return normalizedFallback;
|
||||
}
|
||||
|
||||
const normalizedModelId = resolveFamilyFirstPublishedModelId(rawModelId);
|
||||
const parsedTierModel = parseVscodeServiceTierVariantModelId(normalizedModelId);
|
||||
const canonicalBaseModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId);
|
||||
const parsed = parseModel(canonicalBaseModelId, "");
|
||||
const providerModelId =
|
||||
parsed.model ||
|
||||
(canonicalBaseModelId.includes("/")
|
||||
? canonicalBaseModelId.split("/").slice(1).join("/")
|
||||
: canonicalBaseModelId) ||
|
||||
fallbackValue;
|
||||
|
||||
return humanizeModelIdentifier(providerModelId);
|
||||
}
|
||||
|
||||
function prefixDisplayName(displayName: string, providerPrefix: string | null) {
|
||||
const normalizedProviderPrefix = providerPrefix?.trim() || null;
|
||||
const normalizedDisplayName = normalizeDisplayNameBranding(displayName);
|
||||
|
||||
if (!normalizedProviderPrefix) return normalizedDisplayName;
|
||||
|
||||
const providerTokens = tokenize(normalizedProviderPrefix);
|
||||
if (providerTokens.length === 0) return normalizedDisplayName;
|
||||
|
||||
const displayNameLower = normalizedDisplayName.toLowerCase();
|
||||
if (providerTokens.some((token) => displayNameLower.includes(token))) {
|
||||
return normalizedDisplayName;
|
||||
}
|
||||
|
||||
return `${normalizedProviderPrefix} ${stripLeadingProviderPrefix(normalizedDisplayName, normalizedProviderPrefix)}`.trim();
|
||||
}
|
||||
|
||||
export function resolveVscodeModelMetadata(model: VscodeCatalogModel) {
|
||||
const rawModelId = model.id || model.root || model.name || "";
|
||||
const normalizedModelId = resolveFamilyFirstPublishedModelId(rawModelId);
|
||||
const parsedTierModel = parseVscodeServiceTierVariantModelId(normalizedModelId);
|
||||
const canonicalBaseModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId);
|
||||
const parsed = parseModel(canonicalBaseModelId, "");
|
||||
const provider = parsed.provider || model.owned_by || undefined;
|
||||
const providerModel =
|
||||
parsed.model ||
|
||||
(canonicalBaseModelId.includes("/")
|
||||
? canonicalBaseModelId.split("/").slice(1).join("/")
|
||||
: canonicalBaseModelId) ||
|
||||
model.root ||
|
||||
model.id ||
|
||||
model.name ||
|
||||
undefined;
|
||||
|
||||
return providerModel && provider
|
||||
? getCanonicalModelMetadata({ provider, model: providerModel })
|
||||
: providerModel
|
||||
? getCanonicalModelMetadata({ model: providerModel })
|
||||
: null;
|
||||
}
|
||||
|
||||
export function getVscodeModelDisplayName(model: VscodeCatalogModel) {
|
||||
const rawModelId = model.id || model.root || model.name || "";
|
||||
const { serviceTier } = parseVscodeServiceTierVariantModelId(rawModelId);
|
||||
const metadata = resolveVscodeModelMetadata(model);
|
||||
const displayName = metadata?.displayName || model.name || model.id || model.root || "unknown";
|
||||
const prefixedDisplayName = prefixDisplayName(displayName, getProviderPrefix(metadata));
|
||||
const shouldShowTierSuffix = Boolean(serviceTier) || supportsVscodeServiceTierVariants(model);
|
||||
return shouldShowTierSuffix
|
||||
? `${prefixedDisplayName} (${getVscodeServiceTierVariantSuffix(serviceTier)})`
|
||||
: prefixedDisplayName;
|
||||
}
|
||||
|
||||
export function getVscodeModelGroupingKey(model: VscodeCatalogModel) {
|
||||
const metadata = resolveVscodeModelMetadata(model);
|
||||
return metadata?.qualifiedId || metadata?.model || model.id || model.name || model.root || "";
|
||||
}
|
||||
43
src/app/api/v1/vscode/raw/[token]/models/route.ts
Normal file
43
src/app/api/v1/vscode/raw/[token]/models/route.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
enrichModelForVscode,
|
||||
expandVscodeRawModels,
|
||||
getVscodeModelsCatalogResponse,
|
||||
} from "@/app/api/v1/vscode/[token]/models/route";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/raw/[token]/tokenizedRequest";
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params?: Promise<{ token: string }> | { token: string } } = {}
|
||||
) {
|
||||
const resolvedParams = params ? await params : undefined;
|
||||
const authorizedRequest = withPathTokenApiKey(request, resolvedParams?.token);
|
||||
const catalog = await getVscodeModelsCatalogResponse(authorizedRequest);
|
||||
if (catalog.status < 200 || catalog.status >= 300 || !Array.isArray(catalog.body.data)) {
|
||||
return Response.json(catalog.body, {
|
||||
status: catalog.status,
|
||||
headers: catalog.headers,
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
...catalog.body,
|
||||
data: expandVscodeRawModels(catalog.body.data).map((model) =>
|
||||
enrichModelForVscode(model, authorizedRequest, { preserveNativeId: true })
|
||||
),
|
||||
},
|
||||
{
|
||||
status: catalog.status,
|
||||
headers: catalog.headers,
|
||||
}
|
||||
);
|
||||
}
|
||||
175
src/app/api/v1/vscode/raw/[token]/reasoningMetadata.ts
Normal file
175
src/app/api/v1/vscode/raw/[token]/reasoningMetadata.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { parseModel } from "@omniroute/open-sse/services/model";
|
||||
import { supportsXHighEffort } from "@omniroute/open-sse/config/providerModels";
|
||||
import { stripVscodeServiceTierVariantModelId } from "@/app/api/v1/vscode/raw/[token]/serviceTierVariants";
|
||||
|
||||
export type VscodeCatalogModel = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
root?: string;
|
||||
owned_by?: string;
|
||||
capabilities?: Record<string, boolean>;
|
||||
supportsReasoningEffort?: string[];
|
||||
supportedReasoningEfforts?: string[];
|
||||
supports_reasoning_effort?: string[];
|
||||
defaultReasoningEffort?: string;
|
||||
default_reasoning_effort?: string;
|
||||
};
|
||||
|
||||
const EFFORT_SUFFIX_PATTERN = /-(xhigh|high|medium|low|none)$/i;
|
||||
const DEFAULT_REASONING_EFFORT = "none";
|
||||
const KNOWN_REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh"]);
|
||||
|
||||
export type VscodeModelConfigSchema = {
|
||||
type: "object";
|
||||
properties: {
|
||||
reasoningEffort: {
|
||||
type: "string";
|
||||
title: string;
|
||||
description: string;
|
||||
default: string;
|
||||
enum: string[];
|
||||
enumLabels: string[];
|
||||
enumDescriptions: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export function getCatalogModelName(model: VscodeCatalogModel) {
|
||||
return stripVscodeServiceTierVariantModelId(model.id || model.name || model.root || "");
|
||||
}
|
||||
|
||||
function normalizeReasoningEffortValue(value: string) {
|
||||
const normalized = value.trim().toLowerCase().replace(/[_\s-]+/g, "");
|
||||
if (normalized === "xhigh") return "xhigh";
|
||||
if (KNOWN_REASONING_EFFORTS.has(normalized)) return normalized;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getNativeReasoningEffortValues(model: VscodeCatalogModel) {
|
||||
const candidates = [
|
||||
model.supportsReasoningEffort,
|
||||
model.supportedReasoningEfforts,
|
||||
model.supports_reasoning_effort,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (!Array.isArray(candidate) || candidate.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalized = Array.from(
|
||||
new Set(candidate.map(value => typeof value === "string" ? normalizeReasoningEffortValue(value) : undefined).filter(Boolean))
|
||||
) as string[];
|
||||
|
||||
if (normalized.length > 0) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isReasoningCapableModel(model: VscodeCatalogModel) {
|
||||
return (
|
||||
model.capabilities?.reasoning === true ||
|
||||
model.capabilities?.thinking === true ||
|
||||
(getNativeReasoningEffortValues(model)?.length || 0) > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function getReasoningEffortValues(model: VscodeCatalogModel) {
|
||||
const nativeReasoningEffortValues = getNativeReasoningEffortValues(model);
|
||||
if (nativeReasoningEffortValues && nativeReasoningEffortValues.length > 0) {
|
||||
return nativeReasoningEffortValues;
|
||||
}
|
||||
|
||||
if (!isReasoningCapableModel(model)) return undefined;
|
||||
|
||||
const modelId = getCatalogModelName(model);
|
||||
const parsed = parseModel(modelId, "");
|
||||
const providerId = parsed.provider || model.owned_by || "";
|
||||
const providerModelId = parsed.model || model.root || modelId.split("/").pop() || modelId;
|
||||
const values = ["none", "low", "medium", "high"];
|
||||
|
||||
if (providerId && providerModelId && supportsXHighEffort(providerId, providerModelId)) {
|
||||
values.push("xhigh");
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
export function formatReasoningEffortLabel(level: string) {
|
||||
if (level === "xhigh") return "XHigh";
|
||||
return level.charAt(0).toUpperCase() + level.slice(1);
|
||||
}
|
||||
|
||||
function describeReasoningEffort(level: string) {
|
||||
switch (level) {
|
||||
case "none":
|
||||
return "Disables extra reasoning effort.";
|
||||
case "low":
|
||||
return "Uses a light amount of reasoning.";
|
||||
case "medium":
|
||||
return "Uses a balanced amount of reasoning.";
|
||||
case "high":
|
||||
return "Uses an extended amount of reasoning.";
|
||||
case "xhigh":
|
||||
return "Uses the maximum available reasoning effort.";
|
||||
default:
|
||||
return `Uses ${formatReasoningEffortLabel(level)} reasoning effort.`;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildSupportedReasoningEfforts(
|
||||
supportedValues: string[]
|
||||
): string[] {
|
||||
return [...supportedValues];
|
||||
}
|
||||
|
||||
export function inferSelectedReasoningEffort(
|
||||
model: VscodeCatalogModel,
|
||||
supportedValues?: string[]
|
||||
) {
|
||||
const modelId = getCatalogModelName(model);
|
||||
const match = modelId.match(EFFORT_SUFFIX_PATTERN);
|
||||
if (!match) return undefined;
|
||||
|
||||
const selected = match[1]?.toLowerCase();
|
||||
if (!selected) return undefined;
|
||||
if (Array.isArray(supportedValues) && supportedValues.length > 0 && !supportedValues.includes(selected)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
export function getReasoningVariantBaseModelId(modelId: string) {
|
||||
return modelId.replace(EFFORT_SUFFIX_PATTERN, "");
|
||||
}
|
||||
|
||||
export function getDefaultReasoningEffort(
|
||||
model: VscodeCatalogModel,
|
||||
supportedValues?: string[]
|
||||
) {
|
||||
return inferSelectedReasoningEffort(model, supportedValues) || DEFAULT_REASONING_EFFORT;
|
||||
}
|
||||
|
||||
export function buildReasoningConfigSchema(
|
||||
supportedValues: string[],
|
||||
defaultReasoningEffort: string
|
||||
): VscodeModelConfigSchema {
|
||||
return {
|
||||
type: "object",
|
||||
properties: {
|
||||
reasoningEffort: {
|
||||
type: "string",
|
||||
title: "Reasoning effort",
|
||||
description: "Controls how much reasoning effort the model uses.",
|
||||
default: defaultReasoningEffort,
|
||||
enum: supportedValues,
|
||||
enumLabels: supportedValues.map(formatReasoningEffortLabel),
|
||||
enumDescriptions: supportedValues.map(describeReasoningEffort),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
10
src/app/api/v1/vscode/raw/[token]/responses/route.ts
Normal file
10
src/app/api/v1/vscode/raw/[token]/responses/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { POST as basePost, OPTIONS } from "@/app/api/v1/responses/route";
|
||||
import { rewriteVscodeServiceTierRequest } from "@/app/api/v1/vscode/raw/[token]/serviceTierVariants";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/raw/[token]/tokenizedRequest";
|
||||
|
||||
export { OPTIONS };
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const authorizedRequest = withPathTokenApiKey(request);
|
||||
return basePost(await rewriteVscodeServiceTierRequest(authorizedRequest));
|
||||
}
|
||||
20
src/app/api/v1/vscode/raw/[token]/route.ts
Normal file
20
src/app/api/v1/vscode/raw/[token]/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { CORS_HEADERS } from "@/shared/utils/cors";
|
||||
|
||||
type VscodeTokenParams = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, { headers: CORS_HEADERS });
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
context?: { params: Promise<VscodeTokenParams> | VscodeTokenParams }
|
||||
) {
|
||||
const modelsRoute = await import("@/app/api/v1/vscode/raw/[token]/models/route");
|
||||
const requestUrl = new URL(request.url);
|
||||
requestUrl.pathname = `${requestUrl.pathname.replace(/\/+$/, "")}/models`;
|
||||
const modelsRequest = new Request(requestUrl, request);
|
||||
return modelsRoute.GET(modelsRequest, context);
|
||||
}
|
||||
190
src/app/api/v1/vscode/raw/[token]/serviceTierVariants.ts
Normal file
190
src/app/api/v1/vscode/raw/[token]/serviceTierVariants.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS } from "@/lib/providers/codexFastTier";
|
||||
import { normalizeServiceTierId, type ServiceTierId } from "@/shared/utils/serviceTierLabels";
|
||||
import { resolveFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/raw/[token]/familyFirstModelIds";
|
||||
|
||||
const SERVICE_TIER_VARIANT_PATTERN = /__tier_(priority|flex)$/i;
|
||||
const SUPPORTED_VSCODE_SERVICE_TIERS: readonly ServiceTierId[] = ["priority", "flex"];
|
||||
|
||||
export type VscodeServiceTierModelLike = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
root?: string;
|
||||
owned_by?: string;
|
||||
};
|
||||
|
||||
export function parseVscodeServiceTierVariantModelId(modelId: string | null | undefined): {
|
||||
baseModelId: string;
|
||||
serviceTier?: ServiceTierId;
|
||||
} {
|
||||
const rawModelId = typeof modelId === "string" ? modelId.trim() : "";
|
||||
if (!rawModelId) {
|
||||
return { baseModelId: "" };
|
||||
}
|
||||
|
||||
const match = rawModelId.match(SERVICE_TIER_VARIANT_PATTERN);
|
||||
if (!match) {
|
||||
return { baseModelId: rawModelId };
|
||||
}
|
||||
|
||||
const baseModelId = rawModelId.replace(SERVICE_TIER_VARIANT_PATTERN, "");
|
||||
const serviceTier = normalizeServiceTierId(match[1]);
|
||||
return serviceTier === "standard" ? { baseModelId } : { baseModelId, serviceTier };
|
||||
}
|
||||
|
||||
export function stripVscodeServiceTierVariantModelId(modelId: string | null | undefined): string {
|
||||
return parseVscodeServiceTierVariantModelId(modelId).baseModelId;
|
||||
}
|
||||
|
||||
export function isVscodeServiceTierVariantModelId(modelId: string | null | undefined): boolean {
|
||||
return Boolean(parseVscodeServiceTierVariantModelId(modelId).serviceTier);
|
||||
}
|
||||
|
||||
export function getVscodeServiceTierVariantModelId(
|
||||
baseModelId: string,
|
||||
serviceTier: ServiceTierId
|
||||
): string {
|
||||
if (serviceTier === "standard") {
|
||||
return baseModelId;
|
||||
}
|
||||
return `${baseModelId}__tier_${serviceTier}`;
|
||||
}
|
||||
|
||||
function getRawModelId(model: VscodeServiceTierModelLike): string {
|
||||
return (model.id || model.name || model.root || "").trim();
|
||||
}
|
||||
|
||||
function getModelProvider(model: VscodeServiceTierModelLike, baseModelId: string): string {
|
||||
const owner = typeof model.owned_by === "string" ? model.owned_by.trim().toLowerCase() : "";
|
||||
if (owner) {
|
||||
return owner;
|
||||
}
|
||||
const prefix = baseModelId.split("/")[0]?.trim().toLowerCase() || "";
|
||||
return prefix;
|
||||
}
|
||||
|
||||
function supportsCodexServiceTierModel(baseModelId: string): boolean {
|
||||
const normalizedModel = (baseModelId.split("/").pop() || baseModelId).trim().toLowerCase();
|
||||
if (!normalizedModel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS.some((candidate) => {
|
||||
const normalizedCandidate = candidate.trim().toLowerCase();
|
||||
return normalizedModel === normalizedCandidate || normalizedModel.startsWith(normalizedCandidate);
|
||||
});
|
||||
}
|
||||
|
||||
export function supportsVscodeServiceTierVariants(model: VscodeServiceTierModelLike): boolean {
|
||||
const rawModelId = getRawModelId(model);
|
||||
if (!rawModelId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId);
|
||||
const provider = getModelProvider(model, baseModelId);
|
||||
if (provider !== "codex" && provider !== "cx") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return supportsCodexServiceTierModel(baseModelId);
|
||||
}
|
||||
|
||||
function cloneModelIdentifiers<T extends VscodeServiceTierModelLike>(
|
||||
model: T,
|
||||
modelId: string
|
||||
): T {
|
||||
return {
|
||||
...model,
|
||||
...(model.id ? { id: modelId } : {}),
|
||||
...(model.name ? { name: modelId } : {}),
|
||||
...(model.root ? { root: modelId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function expandVscodeServiceTierModels<T extends VscodeServiceTierModelLike>(models: T[]): T[] {
|
||||
const expanded: T[] = [];
|
||||
|
||||
for (const model of models) {
|
||||
const rawModelId = getRawModelId(model);
|
||||
if (!rawModelId) {
|
||||
expanded.push(model);
|
||||
continue;
|
||||
}
|
||||
|
||||
const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId);
|
||||
const baseModel = rawModelId === baseModelId ? model : cloneModelIdentifiers(model, baseModelId);
|
||||
expanded.push(baseModel as T);
|
||||
|
||||
if (!supportsVscodeServiceTierVariants(model)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const serviceTier of SUPPORTED_VSCODE_SERVICE_TIERS) {
|
||||
expanded.push(cloneModelIdentifiers(baseModel as T, getVscodeServiceTierVariantModelId(baseModelId, serviceTier)));
|
||||
}
|
||||
}
|
||||
|
||||
return expanded;
|
||||
}
|
||||
|
||||
export function getVscodeServiceTierVariantSuffix(serviceTier: ServiceTierId | undefined): string {
|
||||
if (serviceTier === "priority") {
|
||||
return "Fast";
|
||||
}
|
||||
if (serviceTier === "flex") {
|
||||
return "Flex";
|
||||
}
|
||||
return "Default";
|
||||
}
|
||||
|
||||
export function resolveVscodeServiceTierRequest(body: Record<string, unknown>): Record<string, unknown> {
|
||||
const rawModelId = typeof body.model === "string" ? body.model.trim() : "";
|
||||
if (!rawModelId) {
|
||||
return body;
|
||||
}
|
||||
|
||||
const resolvedModelId = resolveFamilyFirstPublishedModelId(rawModelId);
|
||||
|
||||
const { baseModelId, serviceTier } = parseVscodeServiceTierVariantModelId(resolvedModelId);
|
||||
if (!serviceTier) {
|
||||
if (resolvedModelId === rawModelId) {
|
||||
return body;
|
||||
}
|
||||
|
||||
return {
|
||||
...body,
|
||||
model: resolvedModelId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...body,
|
||||
model: baseModelId,
|
||||
...(body.service_tier === undefined ? { service_tier: serviceTier } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function rewriteVscodeServiceTierRequest(request: Request): Promise<Request> {
|
||||
if (request.method !== "POST") {
|
||||
return request;
|
||||
}
|
||||
|
||||
const body = await request.clone().json().catch(() => null);
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
||||
return request;
|
||||
}
|
||||
|
||||
const rewrittenBody = resolveVscodeServiceTierRequest(body as Record<string, unknown>);
|
||||
if (rewrittenBody === body) {
|
||||
return request;
|
||||
}
|
||||
|
||||
const headers = new Headers(request.headers);
|
||||
headers.delete("content-length");
|
||||
|
||||
return new Request(request.url, {
|
||||
method: request.method,
|
||||
headers,
|
||||
body: JSON.stringify(rewrittenBody),
|
||||
});
|
||||
}
|
||||
50
src/app/api/v1/vscode/raw/[token]/tokenizedRequest.ts
Normal file
50
src/app/api/v1/vscode/raw/[token]/tokenizedRequest.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
function inferTokenFromVscodePath(request: Request) {
|
||||
try {
|
||||
const url = new URL(request.url, "http://localhost");
|
||||
const segments = url.pathname
|
||||
.split("/")
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const vscodeIndex = segments.indexOf("vscode");
|
||||
if (vscodeIndex === -1) return null;
|
||||
|
||||
const candidate = segments[vscodeIndex + 1];
|
||||
if (!candidate || candidate === "raw" || candidate === "combos") {
|
||||
const nestedCandidate = segments[vscodeIndex + 2];
|
||||
return nestedCandidate ? decodeURIComponent(nestedCandidate) : null;
|
||||
}
|
||||
|
||||
return decodeURIComponent(candidate);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function withPathTokenApiKey(request: Request, token?: string) {
|
||||
const resolvedToken = token || inferTokenFromVscodePath(request);
|
||||
if (!resolvedToken) return request;
|
||||
|
||||
const headers = new Headers(request.headers);
|
||||
|
||||
if (!headers.has("x-api-key")) {
|
||||
headers.set("x-api-key", resolvedToken);
|
||||
}
|
||||
|
||||
if (!headers.has("authorization")) {
|
||||
headers.set("authorization", `Bearer ${resolvedToken}`);
|
||||
}
|
||||
|
||||
const method = request.method;
|
||||
const init: RequestInit & { duplex?: "half" } = {
|
||||
method,
|
||||
headers,
|
||||
};
|
||||
|
||||
if (method !== "GET" && method !== "HEAD") {
|
||||
init.body = request.body;
|
||||
init.duplex = "half";
|
||||
}
|
||||
|
||||
return new Request(request.url, init);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { POST as basePost, OPTIONS } from "@/app/api/v1/chat/completions/route";
|
||||
import { rewriteVscodeServiceTierRequest } from "@/app/api/v1/vscode/raw/[token]/serviceTierVariants";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/raw/[token]/tokenizedRequest";
|
||||
|
||||
export { OPTIONS };
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const authorizedRequest = withPathTokenApiKey(request);
|
||||
return basePost(await rewriteVscodeServiceTierRequest(authorizedRequest));
|
||||
}
|
||||
1
src/app/api/v1/vscode/raw/[token]/v1/models/route.ts
Normal file
1
src/app/api/v1/vscode/raw/[token]/v1/models/route.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { GET, OPTIONS } from "@/app/api/v1/vscode/raw/[token]/models/route";
|
||||
1207
tests/unit/vscode-token-routes.test.ts
Normal file
1207
tests/unit/vscode-token-routes.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user