Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/cli/src/lib/utils/__tests__/context-window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe("getContextWindow", () => {
[providerIdentifiers.vercelAiGateway, "vercelAiGatewayModelId"],
[providerIdentifiers.opencodeGo, "opencodeGoModelId"],
[providerIdentifiers.kenari, "kenariModelId"],
[providerIdentifiers.nanogpt, "nanoGptModelId"],
[providerIdentifiers.zooGateway, "zooGatewayModelId"],
] as const)("uses the provider-specific model field for %s", (provider, modelField) => {
const config = { apiProvider: provider, [modelField]: "selected-model" } as ProviderSettings
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/lib/utils/context-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ function getModelIdForProvider(config: ProviderSettings): string | undefined {
return config.opencodeGoModelId
case providerIdentifiers.kenari:
return config.kenariModelId
case providerIdentifiers.nanogpt:
return config.nanoGptModelId
case providerIdentifiers.zooGateway:
return config.zooGatewayModelId
case providerIdentifiers.anthropic:
Expand Down
67 changes: 67 additions & 0 deletions packages/types/src/__tests__/nanogpt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
applyNanoGptRoutingPreference,
dynamicProviders,
getModelId,
getProviderDefaultModelId,
isSecretStateKey,
nanoGptDefaultModelId,
nanoGptDefaultRoutingPreference,
providerIdentifiers,
providerSettingsSchema,
} from "../index.js"

describe("NanoGPT shared contract", () => {
it("registers the stable dynamic-provider identity and default model", () => {
expect(providerIdentifiers.nanogpt).toBe("nanogpt")
expect(dynamicProviders).toContain("nanogpt")
expect(getProviderDefaultModelId("nanogpt")).toBe(nanoGptDefaultModelId)
})

it("classifies the API key as secret and resolves missing routing to auto", () => {
expect(isSecretStateKey("nanoGptApiKey")).toBe(true)
const settings = providerSettingsSchema.parse({ apiProvider: "nanogpt", nanoGptModelId: "model" })
expect(settings.nanoGptRoutingPreference ?? nanoGptDefaultRoutingPreference).toBe("auto")
expect(getModelId(settings)).toBe("model")
})
})

describe("applyNanoGptRoutingPreference", () => {
it.each([
["auto", "model"],
["fast", "model:fast"],
["cheap", "model:cheap"],
["latency", "model:latency"],
["throughput", "model:throughput"],
["tools", "model:tools"],
["caching", "model"],
] as const)("maps %s routing", (preference, expected) => {
expect(applyNanoGptRoutingPreference("model", preference)).toBe(expected)
})

it.each([
"speed",
"fast",
"throughput",
"latency",
"price",
"cheap",
"floor",
"tools",
"caching",
"cache",
"cached",
])("replaces the recognized %s routing alias", (alias) => {
expect(applyNanoGptRoutingPreference(`model:thinking:${alias}`, "cheap")).toBe("model:thinking:cheap")
expect(applyNanoGptRoutingPreference(`model:thinking:${alias}`, "auto")).toBe("model:thinking")
})

it("preserves legitimate identity suffixes", () => {
expect(applyNanoGptRoutingPreference("model:thinking", "fast")).toBe("model:thinking:fast")
expect(applyNanoGptRoutingPreference("model:thinking", "auto")).toBe("model:thinking")
})

it("normalizes multiple trailing routing suffixes to exactly one active preference", () => {
expect(applyNanoGptRoutingPreference("model:thinking:fast:cheap", "latency")).toBe("model:thinking:latency")
expect(applyNanoGptRoutingPreference("model:thinking:FAST:CACHED", "auto")).toBe("model:thinking")
})
})
2 changes: 2 additions & 0 deletions packages/types/src/__tests__/provider-identifiers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const expectedProviderIdentifiers = [
"deepseek",
"opencode-go",
"kenari",
"nanogpt",
"ollama",
"lmstudio",
"vscode-lm",
Expand Down Expand Up @@ -106,6 +107,7 @@ describe("provider identifiers", () => {
providerIdentifiers.moonshot,
providerIdentifiers.opencodeGo,
providerIdentifiers.kenari,
providerIdentifiers.nanogpt,
providerIdentifiers.kimiCode,
])
expect(localProviders).toEqual([providerIdentifiers.ollama, providerIdentifiers.lmstudio])
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/__tests__/provider-model-id.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const expectedModelIdKeys = [
"vercelAiGatewayModelId",
"opencodeGoModelId",
"kenariModelId",
"nanoGptModelId",
"zooGatewayModelId",
] as const

Expand Down
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ export const SECRET_STATE_KEYS = [
"vercelAiGatewayApiKey",
"opencodeGoApiKey",
"kenariApiKey",
"nanoGptApiKey",
"basetenApiKey",
] as const

Expand Down
1 change: 1 addition & 0 deletions packages/types/src/provider-identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const providerIdentifiers = {
deepseek: "deepseek",
opencodeGo: "opencode-go",
kenari: "kenari",
nanogpt: "nanogpt",
ollama: "ollama",
lmstudio: "lmstudio",
vscodeLm: "vscode-lm",
Expand Down
7 changes: 7 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export {
OPEN_AI_CODEX_SERVICE_TIER_KEY,
kimiCodeAuthMethodSchema,
type KimiCodeAuthMethod,
nanoGptDefaultRoutingPreference,
nanoGptRoutingPreferences,
nanoGptRoutingPreferenceSchema,
type NanoGptRoutingPreference,
zaiApiLineSchema,
type ZaiApiLine,
} from "./provider-settings/index.js"
Expand Down Expand Up @@ -67,6 +71,7 @@ export const dynamicProviders = [
providerIdentifiers.moonshot,
providerIdentifiers.opencodeGo,
providerIdentifiers.kenari,
providerIdentifiers.nanogpt,
providerIdentifiers.kimiCode,
] as const

Expand Down Expand Up @@ -280,6 +285,7 @@ export const modelIdKeys = [
"vercelAiGatewayModelId",
"opencodeGoModelId",
"kenariModelId",
"nanoGptModelId",
"zooGatewayModelId",
] as const satisfies readonly ModelIdKey[]

Expand Down Expand Up @@ -508,6 +514,7 @@ export const MODELS_BY_PROVIDER: Record<
},
[providerIdentifiers.opencodeGo]: { id: providerIdentifiers.opencodeGo, label: "Opencode Go", models: [] },
[providerIdentifiers.kenari]: { id: providerIdentifiers.kenari, label: "Kenari", models: [] },
[providerIdentifiers.nanogpt]: { id: providerIdentifiers.nanogpt, label: "NanoGPT", models: [] },
[providerIdentifiers.zooGateway]: { id: providerIdentifiers.zooGateway, label: "Zoo Gateway", models: [] },

// Local providers; models discovered from localhost endpoints.
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/provider-settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { qwenCodeProviderDefinition } from "./qwen-code.js"
import { vercelAiGatewayProviderDefinition } from "./vercel-ai-gateway.js"
import { opencodeGoProviderDefinition } from "./opencode-go.js"
import { kenariProviderDefinition } from "./kenari.js"
import { nanoGptProviderDefinition } from "./nanogpt.js"
import { zooGatewayProviderDefinition } from "./zoo-gateway.js"
import { basetenProviderDefinition } from "./baseten.js"

Expand All @@ -38,6 +39,12 @@ import type { ProviderDefinition } from "./common.js"
export { OPEN_AI_CODEX_SERVICE_TIER_KEY } from "./openai-codex.js"
export { kimiCodeAuthMethodSchema, type KimiCodeAuthMethod } from "./kimi-code.js"
export { zaiApiLineSchema, type ZaiApiLine } from "./zai.js"
export {
nanoGptDefaultRoutingPreference,
nanoGptRoutingPreferences,
nanoGptRoutingPreferenceSchema,
type NanoGptRoutingPreference,
} from "./nanogpt.js"
export type { ProviderDefinition } from "./common.js"

export const providerDefinitionList = [
Expand Down Expand Up @@ -73,6 +80,7 @@ export const providerDefinitionList = [
vercelAiGatewayProviderDefinition,
opencodeGoProviderDefinition,
kenariProviderDefinition,
nanoGptProviderDefinition,
zooGatewayProviderDefinition,
basetenProviderDefinition,
] as const satisfies readonly ProviderDefinition[]
24 changes: 24 additions & 0 deletions packages/types/src/provider-settings/nanogpt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { z } from "zod"

import { providerIdentifiers } from "../provider-identifiers.js"
import { baseProviderSettingsShape, createModelIdAccessor, createProviderDefinition } from "./common.js"

const NANOGPT_MODEL_ID_FIELD = "nanoGptModelId"

export const nanoGptRoutingPreferences = ["auto", "fast", "cheap", "latency", "throughput", "tools", "caching"] as const

export const nanoGptRoutingPreferenceSchema = z.enum(nanoGptRoutingPreferences)
export type NanoGptRoutingPreference = z.infer<typeof nanoGptRoutingPreferenceSchema>
export const nanoGptDefaultRoutingPreference: NanoGptRoutingPreference = "auto"

export const nanoGptProviderDefinition = createProviderDefinition({
apiProvider: providerIdentifiers.nanogpt,
modelIdKey: NANOGPT_MODEL_ID_FIELD,
getModelId: createModelIdAccessor(NANOGPT_MODEL_ID_FIELD),
schema: {
...baseProviderSettingsShape,
nanoGptApiKey: z.string().optional(),
[NANOGPT_MODEL_ID_FIELD]: z.string().optional(),
nanoGptRoutingPreference: nanoGptRoutingPreferenceSchema.optional(),
},
})
4 changes: 4 additions & 0 deletions packages/types/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export * from "./xai.js"
export * from "./vercel-ai-gateway.js"
export * from "./opencode-go.js"
export * from "./kenari.js"
export * from "./nanogpt.js"
export * from "./kimi-code.js"
export * from "./zai.js"
export * from "./minimax.js"
Expand Down Expand Up @@ -54,6 +55,7 @@ import { xaiDefaultModelId } from "./xai.js"
import { vercelAiGatewayDefaultModelId } from "./vercel-ai-gateway.js"
import { opencodeGoDefaultModelId } from "./opencode-go.js"
import { kenariDefaultModelId } from "./kenari.js"
import { nanoGptDefaultModelId } from "./nanogpt.js"
import { kimiCodeDefaultModelId } from "./kimi-code.js"
import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId } from "./zai.js"
import { minimaxDefaultModelId } from "./minimax.js"
Expand Down Expand Up @@ -133,6 +135,8 @@ export function getProviderDefaultModelId(
return opencodeGoDefaultModelId
case providerIdentifiers.kenari:
return kenariDefaultModelId
case providerIdentifiers.nanogpt:
return nanoGptDefaultModelId
case providerIdentifiers.kimiCode:
return kimiCodeDefaultModelId
case providerIdentifiers.zooGateway:
Expand Down
57 changes: 57 additions & 0 deletions packages/types/src/providers/nanogpt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { ModelInfo } from "../model.js"
import type { NanoGptRoutingPreference } from "../provider-settings/nanogpt.js"

export const NANOGPT_BASE_URL = "https://nano-gpt.com/api/v1"

export const nanoGptDefaultModelId = "openai/gpt-5.6-sol"

export const nanoGptDefaultModelInfo: ModelInfo = {
maxTokens: 128_000,
contextWindow: 1_050_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 5,
outputPrice: 30,
description: "NanoGPT model. Available models and metadata are resolved dynamically from the detailed catalog.",
}

const ROUTING_SUFFIXES = new Set([
"speed",
"fast",
"throughput",
"latency",
"price",
"cheap",
"floor",
"tools",
"caching",
"cache",
"cached",
])

const ROUTING_SUFFIX_BY_PREFERENCE: Record<Exclude<NanoGptRoutingPreference, "auto" | "caching">, string> = {
fast: "fast",
cheap: "cheap",
latency: "latency",
throughput: "throughput",
tools: "tools",
}

/** Applies one request-only NanoGPT routing suffix while preserving identity suffixes such as `:thinking`. */
export function applyNanoGptRoutingPreference(modelId: string, preference: NanoGptRoutingPreference = "auto"): string {
let canonicalId = modelId
let separatorIndex = canonicalId.lastIndexOf(":")
let finalSuffix = separatorIndex >= 0 ? canonicalId.slice(separatorIndex + 1).toLowerCase() : ""

// Normalize every trailing routing alias. This protects request identity when a
// previously-routed ID is routed again and prevents multiple active suffixes.
while (separatorIndex >= 0 && ROUTING_SUFFIXES.has(finalSuffix)) {
canonicalId = canonicalId.slice(0, separatorIndex)
separatorIndex = canonicalId.lastIndexOf(":")
finalSuffix = separatorIndex >= 0 ? canonicalId.slice(separatorIndex + 1).toLowerCase() : ""
}

return preference === "auto" || preference === "caching"
? canonicalId
: `${canonicalId}:${ROUTING_SUFFIX_BY_PREFERENCE[preference]}`
}
2 changes: 2 additions & 0 deletions src/api/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
MimoHandler,
MistralHandler,
MoonshotHandler,
NanoGptHandler,
OpenAiCodexHandler,
OpenAiHandler,
OpenAiNativeHandler,
Expand Down Expand Up @@ -99,6 +100,7 @@ const expectedHandlers = {
[providerIdentifiers.vercelAiGateway]: VercelAiGatewayHandler,
[providerIdentifiers.opencodeGo]: OpencodeGoHandler,
[providerIdentifiers.kenari]: KenariHandler,
[providerIdentifiers.nanogpt]: NanoGptHandler,
[providerIdentifiers.zooGateway]: ZooGatewayHandler,
[providerIdentifiers.minimax]: MiniMaxHandler,
[providerIdentifiers.baseten]: BasetenHandler,
Expand Down
3 changes: 3 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
VercelAiGatewayHandler,
OpencodeGoHandler,
KenariHandler,
NanoGptHandler,
ZooGatewayHandler,
MiniMaxHandler,
MimoHandler,
Expand Down Expand Up @@ -224,6 +225,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
return new OpencodeGoHandler(options)
case providerIdentifiers.kenari:
return new KenariHandler(options)
case providerIdentifiers.nanogpt:
return new NanoGptHandler(options)
case providerIdentifiers.zooGateway:
return new ZooGatewayHandler(options)
case providerIdentifiers.minimax:
Expand Down
Loading
Loading