fix(ai): validated together login against models endpoint

Together `/login` key validation chat-completed against the hardcoded
model `moonshotai/Kimi-K2.5`, which is not on Together's serverless
catalog. Together rejects it with HTTP 400 `model_not_available`, so no
valid key could ever pass validation.

Switch Together to the model-agnostic `models-endpoint` validation
against `https://api.together.xyz/v1/models`, matching the pattern used
by the other API-key providers.

Fixes #8328
This commit is contained in:
roboomp
2026-08-12 09:11:13 +00:00
parent 06aecdd51f
commit 62ccb64dbe
3 changed files with 71 additions and 3 deletions
+4
View File
@@ -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
+7 -3
View File
@@ -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",
},
});
+60
View File
@@ -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)");
});
});