mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(ai): add Microsoft-Decision-1 classifier to GitHub Copilot (#11679)
Closes #11678 Changes: - `catalog/providers/github-copilot.ts`: add `microsoft-decision-1` as a `typesafe-system-one` classifier (text input, 32768 context, $0.042/M input, Copilot's static headers). - `src/providers/github-copilot.ts`: register the classifier and wrap `typesafeSystemOneApi()` to add `/v1` to the base URL, since Copilot serves `/v1/systemone` and the OAuth login replaces the base URL with the account endpoint. - README and `docs/models.md`: add the model to the classifier tables. Tests: - `test/classifier-models.test.ts`: with an OAuth credential, the request goes to `https://api.enterprise.githubcopilot.com/v1/systemone` with the Copilot headers, and the model stays available although `availableModelIds` lists only chat models. With an API key, it goes to `https://api.individual.githubcopilot.com/v1/systemone`. Both tests fail without the wrapper. - Ran against `api.enterprise.githubcopilot.com` with a Copilot login: bool, choice, and score questions return answers and usage. Co-authored-by: Hongwei Ma <11957602+Marvae@users.noreply.github.com>
This commit is contained in:
parent
ff2c8931cf
commit
0ec20e157b
5 changed files with 98 additions and 7 deletions
|
|
@ -891,7 +891,7 @@ console.log(model.output); // ['image'] or ['image', 'text']
|
|||
|
||||
## Classification
|
||||
|
||||
Classifier models consume structured JSON state, and optionally images, and answer one or more typed questions. They do not use chat or image-generation APIs. TypeSafe's Jev model, Cloudflare's Clef models, and OpenAI's GPT-6 Luna are available from these built-in providers:
|
||||
Classifier models consume structured JSON state, and optionally images, and answer one or more typed questions. They do not use chat or image-generation APIs. TypeSafe's Jev model, Cloudflare's Clef models, OpenAI's GPT-6 Luna, and Microsoft-Decision-1 are available from these built-in providers:
|
||||
|
||||
| Provider | Model IDs | Auth |
|
||||
| --- | --- | --- |
|
||||
|
|
@ -901,6 +901,7 @@ Classifier models consume structured JSON state, and optionally images, and answ
|
|||
| `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |
|
||||
| `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |
|
||||
| `openai` | `gpt-6-luna` | `OPENAI_API_KEY` |
|
||||
| `github-copilot` | `microsoft-decision-1` | `COPILOT_GITHUB_TOKEN` or GitHub Copilot OAuth |
|
||||
|
||||
```typescript
|
||||
import { builtinModels } from '@earendil-works/pi-ai/providers/all';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Api, Model, ThinkingLevelMap } from "../../src/types.ts";
|
||||
import type { Api, ClassifierModel, Model, ThinkingLevelMap } from "../../src/types.ts";
|
||||
import { addMissing, type ChatModel, defineProvider, mergeCompat, mergeThinking, modelsDevModels } from "../define.ts";
|
||||
import { claude55ThinkingLevels, supportsClaudeMidConvoSystemMessages } from "../families/anthropic.ts";
|
||||
import {
|
||||
|
|
@ -67,6 +67,22 @@ const GPT_6_LUNA: Model<"openai-responses"> = {
|
|||
headers: { ...STATIC_HEADERS },
|
||||
};
|
||||
|
||||
// The authenticated Copilot catalog lists this decision model on /v1/systemone (checked 2026-11-02) without
|
||||
// a context window or price, and GitHub's Copilot pricing page does not list it. These are Azure's values,
|
||||
// which OpenRouter and Vercel AI Gateway also list.
|
||||
const MICROSOFT_DECISION_1: ClassifierModel<"typesafe-system-one"> = {
|
||||
type: "classifier",
|
||||
id: "microsoft-decision-1",
|
||||
name: "Microsoft-Decision-1",
|
||||
api: "typesafe-system-one",
|
||||
provider: "github-copilot",
|
||||
baseUrl: BASE_URL,
|
||||
input: ["text"],
|
||||
cost: { input: 0.042, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 32768,
|
||||
headers: { ...STATIC_HEADERS },
|
||||
};
|
||||
|
||||
export default defineProvider({
|
||||
id: "github-copilot",
|
||||
models: (ctx) =>
|
||||
|
|
@ -94,6 +110,7 @@ export default defineProvider({
|
|||
}),
|
||||
[GPT_6_LUNA],
|
||||
).map((model) => (EXTENDED_CONTEXT_MODEL_IDS.has(model.id) ? { ...model, contextWindow: 1000000 } : model)),
|
||||
classifiers: () => [MICROSOFT_DECISION_1],
|
||||
compat: { "openai-completions": { supportsStrictMode: true } },
|
||||
finalize: (model) => {
|
||||
if (model.id.startsWith("gpt-5")) mergeThinking(model, { minimal: "low" });
|
||||
|
|
|
|||
|
|
@ -1,13 +1,28 @@
|
|||
import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts";
|
||||
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
|
||||
import { openAIResponsesApi } from "../api/openai-responses.lazy.ts";
|
||||
import { typesafeSystemOneApi } from "../api/typesafe-system-one.lazy.ts";
|
||||
import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts";
|
||||
import { loadGitHubCopilotOAuth } from "../auth/oauth/load.ts";
|
||||
import { createProvider, type Provider } from "../models.ts";
|
||||
import { GITHUB_COPILOT_MODELS } from "./github-copilot.models.ts";
|
||||
import type { ProviderClassifier } from "../types.ts";
|
||||
import { GITHUB_COPILOT_CLASSIFIER_MODELS, GITHUB_COPILOT_MODELS } from "./github-copilot.models.ts";
|
||||
|
||||
export function githubCopilotProvider(): Provider<"anthropic-messages" | "openai-completions" | "openai-responses"> {
|
||||
return createProvider({
|
||||
type GitHubCopilotApi = "anthropic-messages" | "openai-completions" | "openai-responses";
|
||||
|
||||
/**
|
||||
* Copilot serves System One at `/v1/systemone`, while its chat APIs have no version prefix.
|
||||
* The login replaces the catalog base URL with the account's endpoint, so the prefix is added per request.
|
||||
*/
|
||||
function copilotSystemOne(classifier: ProviderClassifier): ProviderClassifier {
|
||||
return {
|
||||
classify: (model, context, options) =>
|
||||
classifier.classify({ ...model, baseUrl: `${model.baseUrl.replace(/\/+$/u, "")}/v1` }, context, options),
|
||||
};
|
||||
}
|
||||
|
||||
export function githubCopilotProvider(): Provider<GitHubCopilotApi> {
|
||||
return createProvider<GitHubCopilotApi>({
|
||||
id: "github-copilot",
|
||||
name: "GitHub Copilot",
|
||||
baseUrl: "https://api.individual.githubcopilot.com",
|
||||
|
|
@ -15,7 +30,7 @@ export function githubCopilotProvider(): Provider<"anthropic-messages" | "openai
|
|||
apiKey: envApiKeyAuth("GitHub Copilot token", ["COPILOT_GITHUB_TOKEN"]),
|
||||
oauth: lazyOAuth({ name: "GitHub Copilot", isSubscription: true, load: loadGitHubCopilotOAuth }),
|
||||
},
|
||||
models: Object.values(GITHUB_COPILOT_MODELS),
|
||||
models: [...Object.values(GITHUB_COPILOT_MODELS), ...Object.values(GITHUB_COPILOT_CLASSIFIER_MODELS)],
|
||||
filterModels: (models, credential) => {
|
||||
if (credential?.type !== "oauth") return models;
|
||||
const availableModelIds = credential.availableModelIds;
|
||||
|
|
@ -30,5 +45,6 @@ export function githubCopilotProvider(): Provider<"anthropic-messages" | "openai
|
|||
"openai-completions": openAICompletionsApi(),
|
||||
"openai-responses": openAIResponsesApi(),
|
||||
},
|
||||
classifiers: { "typesafe-system-one": copilotSystemOne(typesafeSystemOneApi()) },
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -245,6 +245,62 @@ describe("Models with classifier models", () => {
|
|||
expect((await withOAuth.getAvailable("openai")).some((model) => model.id === "gpt-6-luna")).toBe(true);
|
||||
});
|
||||
|
||||
it("routes GitHub Copilot Microsoft-Decision-1 to the account's /v1/systemone endpoint", async () => {
|
||||
const credentials = new InMemoryCredentialStore();
|
||||
await credentials.modify("github-copilot", async () => ({
|
||||
type: "oauth",
|
||||
access: "tid=test;exp=9999999999;proxy-ep=proxy.enterprise.githubcopilot.com;",
|
||||
refresh: "refresh",
|
||||
expires: Date.now() + 3_600_000,
|
||||
// The login records only picker models; classifiers stay listed.
|
||||
availableModelIds: ["gpt-6-luna"],
|
||||
}));
|
||||
const models = builtinModels({ credentials });
|
||||
const decision = models.getModelOfType("classifier", "github-copilot", "microsoft-decision-1");
|
||||
if (!decision) throw new Error("missing GitHub Copilot decision model");
|
||||
expect(decision).toMatchObject({
|
||||
api: "typesafe-system-one",
|
||||
baseUrl: "https://api.individual.githubcopilot.com",
|
||||
input: ["text"],
|
||||
contextWindow: 32768,
|
||||
});
|
||||
expect(models.getModel("github-copilot", "microsoft-decision-1")).toBeUndefined();
|
||||
expect(await models.getAvailableOfType("classifier", "github-copilot")).toEqual([decision]);
|
||||
|
||||
const requests: Array<{ url: string; headers: Headers }> = [];
|
||||
const result = await models.classify(decision, context, {
|
||||
fetch: async (input, init) => {
|
||||
requests.push({ url: String(input), headers: new Headers(init?.headers) });
|
||||
return Response.json({ answers: { approved: { type: "noul", noul: 0.8 } } });
|
||||
},
|
||||
});
|
||||
|
||||
expect(requests.map((request) => request.url)).toEqual(["https://api.enterprise.githubcopilot.com/v1/systemone"]);
|
||||
expect(requests[0].headers.get("authorization")).toBe(
|
||||
"Bearer tid=test;exp=9999999999;proxy-ep=proxy.enterprise.githubcopilot.com;",
|
||||
);
|
||||
// Copilot rejects requests without an editor version.
|
||||
expect(requests[0].headers.get("editor-version")).toBeTruthy();
|
||||
expect(requests[0].headers.get("copilot-integration-id")).toBe("vscode-chat");
|
||||
expect(result.stopReason).toBe("stop");
|
||||
expect(result.answers.approved).toEqual({ type: "bool", probability: 0.8 });
|
||||
});
|
||||
|
||||
it("adds the System One version prefix to the GitHub Copilot catalog base URL", async () => {
|
||||
const models = builtinModels();
|
||||
const decision = models.getModelOfType("classifier", "github-copilot", "microsoft-decision-1");
|
||||
if (!decision) throw new Error("missing GitHub Copilot decision model");
|
||||
const urls: string[] = [];
|
||||
await models.classify(decision, context, {
|
||||
apiKey: "secret",
|
||||
fetch: async (input) => {
|
||||
urls.push(String(input));
|
||||
return Response.json({ answers: { approved: { type: "noul", noul: 0.8 } } });
|
||||
},
|
||||
});
|
||||
expect(urls).toEqual(["https://api.individual.githubcopilot.com/v1/systemone"]);
|
||||
});
|
||||
|
||||
it("routes OpenRouter classifier models through the System One API", () => {
|
||||
const models = builtinModels();
|
||||
for (const model of getBuiltinClassifierModels("openrouter")) {
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ Compatibility settings should describe verified differences in the endpoint's re
|
|||
|
||||
## Use classifier models
|
||||
|
||||
Classifier models do not chat. They answer typed questions about JSON state: pick one of several choices, answer yes or no, or give a score, each with probabilities. Pi includes TypeSafe's Jev model from these providers, Cloudflare's Clef, Clef Flash, and Clef Omni models from Workers AI and OpenRouter, and OpenAI's GPT-6 Luna through the [Decisions API](https://developers.openai.com/api/docs/guides/decisions):
|
||||
Classifier models do not chat. They answer typed questions about JSON state: pick one of several choices, answer yes or no, or give a score, each with probabilities. Pi includes TypeSafe's Jev model from these providers, Cloudflare's Clef, Clef Flash, and Clef Omni models from Workers AI and OpenRouter, OpenAI's GPT-6 Luna through the [Decisions API](https://developers.openai.com/api/docs/guides/decisions), and Microsoft-Decision-1 through GitHub Copilot:
|
||||
|
||||
| Provider | Model IDs | Authentication |
|
||||
|---|---|---|
|
||||
|
|
@ -151,6 +151,7 @@ Classifier models do not chat. They answer typed questions about JSON state: pic
|
|||
| `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |
|
||||
| `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |
|
||||
| `openai` | `gpt-6-luna` | `OPENAI_API_KEY` |
|
||||
| `github-copilot` | `microsoft-decision-1` | `COPILOT_GITHUB_TOKEN` or `/login` |
|
||||
|
||||
Chat models on a [llama.cpp router](llama-cpp.md#classification) are also listed as classifier models.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue