feat(providers): add GreenPT as an OpenAI-compatible provider (#13024)

Merged with a rebaseline commit added on top of your branch: check:file-size freezes the gateways catalog at 1462 lines, so any new entry fails the gate on arrival. The annotation covers this entry and EURouter's (#13025) together, following the route every previous gateway entry took (#11786 seekai, #10987 logfare, #10668 tabitoken, #10531 freebuff, #11631 1min.ai) — the file is declarative data already split into six family files, so splitting it for two entries would break the semantic-families rule.

Validated in a combined worktree with 13 sibling PRs: 132 focused tests pass, typecheck:core clean, file-size green after the rebaseline.

Thank you for stating plainly what you did not verify. "The endpoint exists and is key-gated; catalog, streaming and tool calls not exercised" is worth more than a confident entry that turns out to be guesswork, and the conservative entry that follows from it — empty models, no capability declared, hasFree false with the billing shape spelled out — is exactly right.
This commit is contained in:
Nguyen Thanh Dat
2026-09-11 04:14:04 +07:00
committed by GitHub
parent 9a56147019
commit 2b9e7fb3ec
7 changed files with 107 additions and 4 deletions

View File

@@ -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.

View File

@@ -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).",
@@ -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,

View File

@@ -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";
@@ -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,

View File

@@ -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,
});

View File

@@ -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",

View File

@@ -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"],
@@ -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.",
},

View File

@@ -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`);
}
});