Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 changelog.d/features/12986-greenpt-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **feat(providers):** Added GreenPT as an OpenAI-compatible API-key provider (`https://api.greenpt.ai/v1`), with live model discovery via `passthroughModels`. No free-inference badge: the published docs describe a free API subscription billed per token, not a free tier.
3 changes: 2 additions & 1 deletion config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"_rebaseline_2026_09_10_mergebatch_v3851_greenpt_eurouter": "/merge-batch 2026-09-10 (v3.8.51), PRs #13024 (GreenPT, closes #12986) and #13025 (EURouter, closes #12985) by ntdatt812: src/shared/constants/providers/apikey/gateways.ts 1462->1502 (+40 = two APIKEY_PROVIDERS_GATEWAYS catalog entries, declarative data only: id/alias/name/icon/color/website plus the hasFree=false rationale comments and the apiHint copy each PR verified). No logic and no new branching. Same god-file no-split rationale as every prior gateways.ts rebaseline (#11786 seekai, #10987 logfare, #10668 tabitoken, #10531 freebuff, #11631 1min.ai): the file header says it is pure data merged by apikey/index.ts via spread, and it is already split into 6 family files under apikey/, so splitting a catalog for two entries would violate the semantic-families rule rather than help. Both entries are deliberately conservative (models: [] with passthroughModels, no tool/vision capability declared, hasFree false), so the growth is the entry itself, not claims. EURouter is in AGGREGATOR_PROVIDER_IDS because it routes to third-party upstreams; GreenPT is not because it serves its own inference. Covered by tests/unit/greenpt-provider.test.ts and tests/unit/eurouter-provider.test.ts.",
"_rebaseline_2026_09_10_12828_translate_usage_chunk": "PR #12828 own growth: open-sse/utils/stream.ts 3072->3080 (+8). Translate-mode streams now send the estimated usage as the canonical trailing usage-only chunk before [DONE] when the upstream stays silent (parity with the #12151 passthrough flush), with a latch so a finish chunk that already carried the estimate is not doubled. The chunk builder is shared with the passthrough flush in open-sse/utils/usageOnlyChunk.ts (under cap); what remains is the flush-site wiring. Covered by tests/unit/stream-translate-usage-trailing.test.ts.",
"_rebaseline_2026_09_10_12715_queue_budget": "PR #12715 own growth: open-sse/handlers/chatCore.ts 6021->6036 (+15). Hierarchical admission now resolves the per-connection queue budget before the gates and hands withRateLimit the remaining budget, the correlation id and the executor timeout context, so gate wait, provider slot and Bottleneck queue share one bound instead of stacking. Error shaping lives in open-sse/handlers/chatCore/queueBudget.ts (under cap); what remains is irreducible call-site wiring. Covered by tests/unit/rate-limit-remaining-budget.test.ts, rate-limit-manager-queue-bound.test.ts and chatcore-hierarchical-admission.test.ts.",
"_rebaseline_2026_09_06_runtime_quotagroup_nodemap": "Own growth: src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx 1201->1222 (+21, check-file-size split-newline). QuotaGroup is a module-level sibling and was reading nodeMap from RuntimePageClient's closure; that identifier is not in scope, so a quota monitor with status error/exhausted/alerting throws ReferenceError. Fix threads nodeMap as a prop (3 call sites + parameter + ProviderNodeEntry import). Prettier wraps the long import and the three QuotaGroup JSX tags. Covered by tests/unit/ui/runtime-page-client.test.tsx (empty monitors stay green; error+exhausted fixtures mount QuotaGroup).",
Expand Down Expand Up @@ -465,7 +466,7 @@
"src/lib/tailscaleTunnel.ts": 1208,
"src/lib/tokenHealthCheck.ts": 1218,
"src/shared/components/RequestLoggerV2.tsx": 1718,
"src/shared/constants/providers/apikey/gateways.ts": 1462,
"src/shared/constants/providers/apikey/gateways.ts": 1502,
"src/shared/services/cliRuntime.ts": 1296,
"src/sse/handlers/chat.ts": 2458,
"src/sse/services/auth.ts": 3450,
Expand Down
2 changes: 2 additions & 0 deletions open-sse/config/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ import { electronhubProvider } from "./registry/electronhub/index.ts";
import { llmgatewayProvider } from "./registry/llmgateway/index.ts";
import { llmKiwiProvider } from "./registry/llm-kiwi/index.ts";
import { literouterProvider } from "./registry/literouter/index.ts";
import { greenptProvider } from "./registry/greenpt/index.ts";
import { mnnAiProvider } from "./registry/mnn-ai/index.ts";
import { meganovaAiProvider } from "./registry/meganova-ai/index.ts";
import { mixlayerProvider } from "./registry/mixlayer/index.ts";
Expand Down Expand Up @@ -524,6 +525,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
llmgateway: llmgatewayProvider,
"llm-kiwi": llmKiwiProvider,
literouter: literouterProvider,
greenpt: greenptProvider,
"mnn-ai": mnnAiProvider,
"meganova-ai": meganovaAiProvider,
mixlayer: mixlayerProvider,
Expand Down
11 changes: 11 additions & 0 deletions open-sse/config/providers/registry/greenpt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";

export const greenptProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "greenpt",
alias: "greenpt",
baseUrl: "https://api.greenpt.ai/v1/chat/completions",
modelsUrl: "https://api.greenpt.ai/v1/models",
models: [],
passthroughModels: true,
});
1 change: 1 addition & 0 deletions src/shared/constants/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const PROVIDER_ENDPOINTS = {
llmgateway: "https://api.llmgateway.io/v1/chat/completions",
"llm-kiwi": "https://api.llm.kiwi/v1/chat/completions",
literouter: "https://api.literouter.com/v1/chat/completions",
greenpt: "https://api.greenpt.ai/v1/chat/completions",
"mnn-ai": "https://api.mnnai.ru/v1/chat/completions",
"meganova-ai": "https://api.meganova.ai/v1/chat/completions",
mixlayer: "https://models.mixlayer.ai/v1/chat/completions",
Expand Down
25 changes: 22 additions & 3 deletions src/shared/constants/providers/apikey/gateways.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,25 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
apiHint:
"Create a LiteRouter API key, then use https://api.literouter.com/v1 as the OpenAI-compatible base URL.",
},
greenpt: {
id: "greenpt",
serviceKinds: ["llm"],
alias: "greenpt",
name: "GreenPT",
icon: "eco",
color: "#15803D",
textIcon: "GPT",
passthroughModels: true,
website: "https://greenpt.com",
// Not a free tier. The published docs describe a free API subscription with
// pay-per-token inference, which is a billing shape rather than free usage,
// so this stays false and the note says only what the docs say (#12986).
hasFree: false,
freeNote:
"API subscription is free to create; inference is billed per token. No free inference allowance is published.",
apiHint:
"Create a GreenPT API key, then use https://api.greenpt.ai/v1 as the OpenAI-compatible base URL. Review jurisdiction, privacy and regional data-transfer requirements before use.",
},
"mnn-ai": {
id: "mnn-ai",
serviceKinds: ["llm"],
Expand Down Expand Up @@ -1452,9 +1471,9 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
passthroughModels: true,
website: "https://seekai.cc",
hasFree: true,
freeNote: "Signup credit toward available models; amount and eligibility are set by SeekAi, not OmniRoute.",
authHint:
"Create an API key at https://seekai.cc, then paste it here as a Bearer token.",
freeNote:
"Signup credit toward available models; amount and eligibility are set by SeekAi, not OmniRoute.",
authHint: "Create an API key at https://seekai.cc, then paste it here as a Bearer token.",
apiHint:
"Create an API key at https://seekai.cc, then paste it here as a Bearer token. OpenAI-compatible base URL: https://seekai.cc/v1.",
},
Expand Down
68 changes: 68 additions & 0 deletions tests/unit/greenpt-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import assert from "node:assert/strict";
import test from "node:test";

import { greenptProvider } from "../../open-sse/config/providers/registry/greenpt/index.ts";

const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
const { DefaultExecutor, getExecutor } = await import("../../open-sse/executors/index.ts");
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
const { isValidModel } = await import("../../src/shared/constants/models.ts");
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts");
const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts");

const CHAT_URL = "https://api.greenpt.ai/v1/chat/completions";
const MODELS_URL = "https://api.greenpt.ai/v1/models";

test("greenpt is an OpenAI-compatible Bearer registry entry", () => {
assert.equal(greenptProvider.id, "greenpt");
assert.equal(greenptProvider.alias, "greenpt");
assert.equal(greenptProvider.format, "openai");
assert.equal(greenptProvider.executor, "default");
assert.equal(greenptProvider.authType, "apikey");
assert.equal(greenptProvider.authHeader, "bearer");
assert.equal(greenptProvider.baseUrl, CHAT_URL);
assert.equal(greenptProvider.modelsUrl, MODELS_URL);
assert.equal(greenptProvider.passthroughModels, true);
});

test("greenpt leaves model discovery to the live upstream catalog", () => {
// No account was available to enumerate the catalog, so nothing is hardcoded:
// an empty list plus passthroughModels is the honest shape.
assert.deepEqual(greenptProvider.models, []);
});

test("greenpt is wired through registry, metadata, endpoint and default executor", async () => {
assert.equal(REGISTRY.greenpt?.baseUrl, CHAT_URL);
assert.equal(PROVIDER_ENDPOINTS.greenpt, CHAT_URL);
assert.equal(APIKEY_PROVIDERS.greenpt?.id, "greenpt");
assert.equal(APIKEY_PROVIDERS.greenpt?.alias, "greenpt");
assert.ok((await getExecutor("greenpt")) instanceof DefaultExecutor);
});

test("greenpt accepts any model name the upstream catalog returns", () => {
// passthroughModels drives PASSTHROUGH_PROVIDERS, which is what isValidModel
// consults -- membership of AGGREGATOR_PROVIDER_IDS is not what gates this.
assert.equal(isValidModel("greenpt", "future/live-catalog-model"), true);
});

test("greenpt is not listed as an aggregator", () => {
// It is an inference provider, not a router over other providers, which is
// what that set means. Listing it there would misdescribe it in the UI.
assert.equal(AGGREGATOR_PROVIDER_IDS.has("greenpt"), false);
});

test("greenpt advertises no free inference allowance", () => {
// The published docs describe a free API subscription with pay-per-token
// inference. That is a billing shape, not a free tier, and hasFree drives a
// "Free" badge in the picker.
assert.equal(APIKEY_PROVIDERS.greenpt?.hasFree, false);
});

test("greenpt claims no capability that was not exercised", () => {
// #12986 asks that tool support be advertised only if exercised. No key was
// available, so the entry carries no tool/vision capability declaration.
const metadata = APIKEY_PROVIDERS.greenpt as Record<string, unknown>;
for (const key of ["supportsTools", "supportsVision", "capabilities"]) {
assert.equal(metadata[key], undefined, `${key} must not be declared unverified`);
}
});
Loading