diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 326853dd8..85801b32b 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `/login together` always failing with HTTP 400 `model_not_available`: key validation chat-completed against the hardcoded non-serverless model `moonshotai/Kimi-K2.5`, so no valid key could pass. Validation now probes Together's authenticated `/v1/models` listing, matching the model-agnostic approach used by other API-key providers ([#8328](https://github.com/can1357/oh-my-pi/issues/8328)). + ## [17.2.15] - 2026-08-12 ### Fixed diff --git a/packages/ai/src/registry/together.ts b/packages/ai/src/registry/together.ts index f6731300e..961dd44f5 100644 --- a/packages/ai/src/registry/together.ts +++ b/packages/ai/src/registry/together.ts @@ -8,10 +8,14 @@ export const loginTogether = createApiKeyLogin({ promptMessage: "Paste your Together API key", placeholder: "sk-...", validation: { - kind: "chat-completions", + // Validate against the authenticated models listing, not a chat + // completion: Together rejects models that only exist behind a dedicated + // endpoint (e.g. `moonshotai/Kimi-K2.5`) with an HTTP 400 + // `model_not_available`, which failed key validation for every valid key + // (issue #8328). The `/v1/models` listing is model-agnostic. + kind: "models-endpoint", provider: "together", - baseUrl: "https://api.together.xyz/v1", - model: "moonshotai/Kimi-K2.5", + modelsUrl: "https://api.together.xyz/v1/models", }, }); diff --git a/packages/ai/test/issue-8328-repro.test.ts b/packages/ai/test/issue-8328-repro.test.ts new file mode 100644 index 000000000..01a18f95c --- /dev/null +++ b/packages/ai/test/issue-8328-repro.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, test, vi } from "bun:test"; +import { loginTogether } from "../src/registry/together"; +import type { FetchImpl } from "../src/types"; + +// Together's serverless API rejects models that only exist behind a dedicated +// endpoint (e.g. `moonshotai/Kimi-K2.5`) with an HTTP 400 `model_not_available` +// error — even when the pasted key is perfectly valid. Login validation must +// therefore not depend on chat-completing against a specific model; it must +// probe an authenticated, model-agnostic endpoint. Regression guard for #8328. +const NON_SERVERLESS_400 = { + id: "ovsZQhk-2kFHot", + error: { + message: + "Unable to access non-serverless model moonshotai/Kimi-K2.5. Please visit https://api.together.ai/models/moonshotai/Kimi-K2.5 to create and start a new dedicated endpoint for the model.", + type: "invalid_request_error", + param: null, + code: "model_not_available", + }, +}; + +describe("Together login (#8328)", () => { + test("validates a valid key against the models endpoint, not a hardcoded model", async () => { + const requests: Array<{ url: string; method: string | undefined }> = []; + const fetchMock: FetchImpl = vi.fn(async (input: string | URL | Request, init?: RequestInit) => { + const url = String(input); + requests.push({ url, method: init?.method }); + // Simulate Together: chat-completions with a non-serverless model 400s, + // while the authenticated models listing succeeds for a valid key. + if (url.endsWith("/chat/completions")) { + return Response.json(NON_SERVERLESS_400, { status: 400 }); + } + if (url.endsWith("/models")) { + return Response.json({ object: "list", data: [] }); + } + return Response.json({ error: "unexpected" }, { status: 500 }); + }); + + const apiKey = await loginTogether({ + onPrompt: async () => " together-valid-key ", + fetch: fetchMock, + }); + + expect(apiKey).toBe("together-valid-key"); + // The only validation request must be the authenticated models listing. + expect(requests).toEqual([{ url: "https://api.together.xyz/v1/models", method: "GET" }]); + }); + + test("still rejects an invalid key", async () => { + const fetchMock: FetchImpl = vi.fn(async () => + Response.json({ error: { message: "Invalid API key provided" } }, { status: 401 }), + ); + + await expect( + loginTogether({ + onPrompt: async () => "bad-key", + fetch: fetchMock, + }), + ).rejects.toThrow("together API key validation failed (401)"); + }); +});