fix(ai): validated Fireworks login against control-plane catalog

The login validator pinged `/inference/v1/models`, which Fireworks
serves from the per-account deployment registry and 500s with
`Error listing deployed models` for accounts without active
deployments. Valid `fw_…` keys were rejected during `/login`.

Switched validation to the static control-plane `List Models` API
(`GET /v1/accounts/fireworks/models?filter=supports_serverless=true&pageSize=1`),
the same endpoint discovery already uses. Authentication no longer
depends on the caller owning any deployments.

Fixes #3219
This commit is contained in:
roboomp
2026-06-22 00:15:41 +00:00
parent 0a1323ae80
commit 9cb12da08c
3 changed files with 72 additions and 1 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed `/login fireworks` rejecting valid `fw_…` keys with `Fireworks API key validation failed (500): Error listing deployed models`. The validator pinged `/inference/v1/models`, which Fireworks serves from the per-account deployment registry and 500s for accounts without active deployments. Login now hits the static control-plane `List Models` catalog (`GET /v1/accounts/fireworks/models?filter=supports_serverless=true&pageSize=1`) — the same endpoint discovery already uses — so authentication no longer depends on the caller's deployment state. ([#3219](https://github.com/can1357/oh-my-pi/issues/3219))
## [16.1.11] - 2026-06-21
### Fixed
+7 -1
View File
@@ -11,7 +11,13 @@ export const loginFireworks = createApiKeyLogin({
validation: {
kind: "models-endpoint",
provider: "Fireworks",
modelsUrl: "https://api.fireworks.ai/inference/v1/models",
// The OpenAI-compatible inference listing (`/inference/v1/models`) enumerates
// the caller's *deployed* models and returns `500 Error listing deployed models`
// for accounts without active deployments, which rejected valid `fw_…` keys
// during `/login`. The control-plane `List Models` API hits the static
// `fireworks` serverless catalog (same endpoint discovery uses) and only
// requires the key to authenticate, not to own any deployments.
modelsUrl: "https://api.fireworks.ai/v1/accounts/fireworks/models?filter=supports_serverless%3Dtrue&pageSize=1",
},
});
+61
View File
@@ -0,0 +1,61 @@
/**
* Regression for issue #3219.
*
* The legacy validator pinged `https://api.fireworks.ai/inference/v1/models`,
* which Fireworks serves from the per-account deployment registry and 500s
* (`Error listing deployed models`) for accounts without active deployments.
* That rejected valid `fw_…` keys during `/login`. Validation now uses the
* static control-plane `List Models` API — the same endpoint discovery hits —
* so login only fails for keys that fail to authenticate, not for accounts in
* a "no deployments" state.
*/
import { describe, expect, it } from "bun:test";
import { loginFireworks } from "@oh-my-pi/pi-ai/registry/fireworks";
import type { FetchImpl } from "@oh-my-pi/pi-catalog/types";
const CONTROL_PLANE_HOST = "api.fireworks.ai";
const CONTROL_PLANE_PATH = "/v1/accounts/fireworks/models";
function makeController(fetchImpl: FetchImpl): Parameters<typeof loginFireworks>[0] {
return {
fetch: fetchImpl,
onPrompt: async () => "fw_TESTKEY",
onAuth: () => {},
onProgress: () => {},
};
}
describe("loginFireworks", () => {
it("validates the API key against the control-plane List Models endpoint", async () => {
let capturedUrl = "";
let capturedAuth = "";
const fetchImpl: FetchImpl = async (input, init) => {
capturedUrl = typeof input === "string" ? input : input.toString();
const header = (init?.headers as Record<string, string> | undefined)?.Authorization;
capturedAuth = header ?? "";
return new Response(JSON.stringify({ models: [] }), { status: 200 });
};
const key = await loginFireworks(makeController(fetchImpl));
expect(key).toBe("fw_TESTKEY");
expect(capturedUrl).not.toBe("");
const url = new URL(capturedUrl);
expect(url.host).toBe(CONTROL_PLANE_HOST);
expect(url.pathname).toBe(CONTROL_PLANE_PATH);
expect(url.searchParams.get("filter")).toBe("supports_serverless=true");
// The inference listing — which returned 500 on the reporter's account — must NOT be hit.
expect(url.pathname).not.toBe("/inference/v1/models");
expect(capturedAuth).toBe("Bearer fw_TESTKEY");
});
it("surfaces upstream auth failures with status and body", async () => {
const fetchImpl: FetchImpl = async () =>
new Response("invalid api key", { status: 401, statusText: "Unauthorized" });
await expect(loginFireworks(makeController(fetchImpl))).rejects.toThrow(
/Fireworks API key validation failed \(401\): invalid api key/,
);
});
});