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:
m3 2026-11-02 23:12:43 +08:00 • committed by GitHub
parent ff2c8931cf
commit 0ec20e157b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 98 additions and 7 deletions

View file

@ -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';

View file

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

View file

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

View file

@ -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")) {

View file

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