From f3f7d51c8118572ab19e3b4886abe1128ab1c7ab Mon Sep 17 00:00:00 2001 From: Mansur Nurmukhambetov Date: Sat, 31 Jan 2026 10:53:56 +0500 Subject: [PATCH 01/10] docs: update tools example to clarify AI SDK tool limitations and usage recommendations --- examples/tools.ts | 3 +++ src/provider/types.ts | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/tools.ts b/examples/tools.ts index 8c498e4..bbc68d7 100644 --- a/examples/tools.ts +++ b/examples/tools.ts @@ -1,6 +1,9 @@ /** * Tools example - using tools with GitHub Copilot * + * NOTE: AI SDK tools (tool() with Zod schemas and execute) are not yet supported. + * Use Copilot's defineTool with parameters and handler instead, as shown below. + * * Prerequisites: * - Copilot CLI installed and authenticated * diff --git a/src/provider/types.ts b/src/provider/types.ts index c0b801d..4304340 100644 --- a/src/provider/types.ts +++ b/src/provider/types.ts @@ -29,8 +29,9 @@ export interface GitHubCopilotSettings { /** * Custom tools exposed to the Copilot CLI. + * Use Copilot's defineTool; tool inputs are objects with string/number/nested values. */ - tools?: Tool[]; + tools?: Tool[]; /** * Custom provider configuration (BYOK - Bring Your Own Key). From 16c9a670e734743f470d0435516909bea6df7a8d Mon Sep 17 00:00:00 2001 From: Mansur Nurmukhambetov Date: Sat, 31 Jan 2026 10:57:52 +0500 Subject: [PATCH 02/10] feat: support AI SDK tools via providerOptions bridge Extract tools from AI SDK when execute is passed via providerOptions['github-copilot'].execute and convert them to Copilot defineTool format. Merge with settings.tools. - Add convertAiSdkToolsToCopilotTools conversion - Integrate into session setup and buildSessionConfig - Add examples/tools-ai-sdk.ts and README docs --- README.md | 37 ++++++++- examples/tools-ai-sdk.ts | 53 ++++++++++++ package.json | 1 + .../convert-ai-sdk-tools-to-copilot.ts | 67 +++++++++++++++ src/conversion/index.ts | 1 + src/model/github-copilot-language-model.ts | 13 ++- src/model/session-setup.ts | 7 +- .../convert-ai-sdk-tools-to-copilot.test.ts | 83 +++++++++++++++++++ tests/model/session-setup.test.ts | 18 ++-- 9 files changed, 264 insertions(+), 16 deletions(-) create mode 100644 examples/tools-ai-sdk.ts create mode 100644 src/conversion/convert-ai-sdk-tools-to-copilot.ts create mode 100644 tests/conversion/convert-ai-sdk-tools-to-copilot.test.ts diff --git a/README.md b/README.md index 5827799..049ec2b 100644 --- a/README.md +++ b/README.md @@ -82,10 +82,9 @@ const model = githubCopilot("gpt-5", { ### Custom tools -> [!NOTE] -> AI SDK tools (using `tool()` with Zod schemas and `execute`) are not yet supported. The provider does not receive the `execute` function from the AI SDK, while Copilot requires a `handler` for each tool. The schema mapping (name, description, inputSchema) is compatible; the blocker is the missing execute/handler bridge. See [examples/tools.ts](examples/tools.ts) for how to define tools using the native Copilot API. +You can pass tools in two ways: -Pass tools via provider settings using Copilot's `defineTool`. Tool support varies by model; verify with Copilot CLI or documentation. +**1. Copilot's `defineTool`** (model settings) — use when configuring the model: ```typescript import { defineTool } from "@github/copilot-sdk"; @@ -107,6 +106,38 @@ const model = githubCopilot("gpt-5", { }); ``` +**2. AI SDK `tool()` with `providerOptions`** (call-level tools) — pass `execute` via `providerOptions['github-copilot']` so the provider can use it as the Copilot handler: + +```typescript +import { tool } from "ai"; +import { z } from "zod"; +import { githubCopilot } from "@nomomon/ai-sdk-provider-github-copilot"; +import { streamText } from "ai"; + +const execute = async ({ city }: { city: string }) => ({ + city, + temperature: `${20 + Math.floor(Math.random() * 15)}°C`, + condition: "sunny", +}); + +const getWeather = tool({ + description: "Get the current weather for a city", + inputSchema: z.object({ + city: z.string().describe("The city name"), + }), + execute, + providerOptions: { "github-copilot": { execute } }, +}); + +const result = streamText({ + model: githubCopilot("gpt-5-mini"), + tools: { get_weather: getWeather }, + prompt: "What's the weather in Tokyo?", +}); +``` + +Tool support varies by model; verify with Copilot CLI or documentation. + ## Development Tests live in `tests/` (not colocated with source) to keep the published `src/` tree clean and to exclude test files from the build. Each source module has a corresponding `tests/*.test.ts` file. Run `npm test` or `npm run test:coverage` for coverage with thresholds. diff --git a/examples/tools-ai-sdk.ts b/examples/tools-ai-sdk.ts new file mode 100644 index 0000000..b9a00e8 --- /dev/null +++ b/examples/tools-ai-sdk.ts @@ -0,0 +1,53 @@ +/** + * AI SDK tools example - using tool() with providerOptions bridge + * + * The AI SDK does not pass the tool's execute function to providers. + * Pass it via providerOptions['github-copilot'].execute so the provider + * can use it as the Copilot handler. + * + * Prerequisites: + * - Copilot CLI installed and authenticated + * + * Run: npm run example:tools-ai-sdk + */ + +import { githubCopilot } from "@nomomon/ai-sdk-provider-github-copilot"; +import { streamText, tool } from "ai"; +import { z } from "zod"; + +async function main() { + console.log("Sending prompt with AI SDK tools to GitHub Copilot...\n"); + + const execute = async ({ city }: { city: string }) => { + const conditions = ["sunny", "cloudy", "rainy", "partly cloudy"]; + const temp = Math.floor(Math.random() * 10) + 20; + const condition = conditions[Math.floor(Math.random() * conditions.length)]; + return { city, temperature: `${temp}°C`, condition }; + }; + + const getWeather = tool({ + description: "Get the current weather for a city", + inputSchema: z.object({ + city: z.string().describe("The city name"), + }), + execute, + providerOptions: { "github-copilot": { execute } }, + }); + + const result = streamText({ + model: githubCopilot("gpt-5-mini"), + tools: { get_weather: getWeather }, + prompt: "What's the weather like in San Francisco? Use the get_weather tool.", + }); + + process.stdout.write("Response: "); + for await (const chunk of result.textStream) { + process.stdout.write(chunk); + } + console.log("\n\nDone."); +} + +main().catch((err) => { + console.error("Error:", err.message); + process.exit(1); +}); diff --git a/package.json b/package.json index 6a4406d..6f85f7d 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "example:basic": "npm run build && npx tsx examples/basic-usage.ts", "example:streaming": "npm run build && npx tsx examples/streaming.ts", "example:tools": "npm run build && npx tsx examples/tools.ts", + "example:tools-ai-sdk": "npm run build && npx tsx examples/tools-ai-sdk.ts", "prepare": "lefthook install", "prepublishOnly": "npm run build" }, diff --git a/src/conversion/convert-ai-sdk-tools-to-copilot.ts b/src/conversion/convert-ai-sdk-tools-to-copilot.ts new file mode 100644 index 0000000..9c2154a --- /dev/null +++ b/src/conversion/convert-ai-sdk-tools-to-copilot.ts @@ -0,0 +1,67 @@ +/** + * Converts AI SDK tools to Copilot Tool format when they include + * providerOptions['github-copilot'].execute (the handler bridge). + * + * The AI SDK does not pass the tool's `execute` function to providers. + * Users can pass it via providerOptions as a workaround: + * + * tool({ + * description: '...', + * inputSchema: z.object({...}), + * execute: myHandler, + * providerOptions: { 'github-copilot': { execute: myHandler } }, + * }) + * + * This extracts those tools and converts them to Copilot's defineTool format. + */ + +import type { LanguageModelV3FunctionTool } from "@ai-sdk/provider"; +import type { Tool } from "@github/copilot-sdk"; +import { defineTool } from "@github/copilot-sdk"; + +const PROVIDER_KEY = "github-copilot"; + +type CopilotProviderOptions = { + execute?: (args: unknown) => unknown | Promise; +}; + +function hasExecute( + opts: unknown, +): opts is CopilotProviderOptions & { execute: (args: unknown) => unknown | Promise } { + return ( + opts != null && + typeof opts === "object" && + "execute" in opts && + typeof (opts as CopilotProviderOptions).execute === "function" + ); +} + +/** + * Converts AI SDK function tools that have providerOptions['github-copilot'].execute + * into Copilot Tool format. Tools without the execute bridge are skipped. + */ +export function convertAiSdkToolsToCopilotTools( + tools: Array | undefined, +): Tool[] { + if (!tools?.length) return []; + + const result: Tool[] = []; + + for (const tool of tools) { + if (tool.type !== "function" || !("inputSchema" in tool)) continue; + + const copilotOpts = tool.providerOptions?.[PROVIDER_KEY]; + if (!hasExecute(copilotOpts)) continue; + + const execute = copilotOpts.execute; + const copilotTool = defineTool(tool.name, { + description: tool.description, + parameters: tool.inputSchema as Record, + handler: async (args: unknown) => execute(args), + }); + + result.push(copilotTool); + } + + return result; +} diff --git a/src/conversion/index.ts b/src/conversion/index.ts index 6807bf9..6229a62 100644 --- a/src/conversion/index.ts +++ b/src/conversion/index.ts @@ -1,3 +1,4 @@ +export { convertAiSdkToolsToCopilotTools } from "./convert-ai-sdk-tools-to-copilot.js"; export type { ConvertedCopilotMessage } from "./convert-to-copilot-messages.js"; export { convertToCopilotMessages } from "./convert-to-copilot-messages.js"; export { mapCopilotFinishReason } from "./map-copilot-finish-reason.js"; diff --git a/src/model/github-copilot-language-model.ts b/src/model/github-copilot-language-model.ts index 0c48421..bdb346a 100644 --- a/src/model/github-copilot-language-model.ts +++ b/src/model/github-copilot-language-model.ts @@ -9,6 +9,7 @@ import type { } from "@ai-sdk/provider"; import { generateId } from "@ai-sdk/provider-utils"; import type { CopilotClient } from "@github/copilot-sdk"; +import { convertAiSdkToolsToCopilotTools } from "../conversion/convert-ai-sdk-tools-to-copilot.js"; import { mapCopilotFinishReason } from "../conversion/map-copilot-finish-reason.js"; import type { CopilotUsageEvent } from "../conversion/usage.js"; import { convertCopilotUsage, createEmptyUsage } from "../conversion/usage.js"; @@ -64,13 +65,19 @@ export class GitHubCopilotLanguageModel implements LanguageModelV3 { return this.settings.model ?? this.modelId; } - private buildSessionConfig(streaming: boolean) { + private buildSessionConfig(streaming: boolean, callOptions: LanguageModelV3CallOptions) { + const aiSdkTools = convertAiSdkToolsToCopilotTools(callOptions.tools); + const tools = + aiSdkTools.length > 0 || this.settings.tools?.length + ? [...(this.settings.tools ?? []), ...aiSdkTools] + : undefined; + return { model: this.getEffectiveModel(), sessionId: this.settings.sessionId, streaming, systemMessage: this.settings.systemMessage, - tools: this.settings.tools, + tools, provider: this.settings.provider, workingDirectory: this.settings.workingDirectory, }; @@ -104,7 +111,7 @@ export class GitHubCopilotLanguageModel implements LanguageModelV3 { prompt: options.prompt, options, streaming, - buildSessionConfig: (s) => this.buildSessionConfig(s), + buildSessionConfig: (s, o) => this.buildSessionConfig(s, o), generateWarnings: (o) => this.generateWarnings(o), getClient: this.getClient, systemMessageFromSettings: this.settings.systemMessage, diff --git a/src/model/session-setup.ts b/src/model/session-setup.ts index 1257849..efa3845 100644 --- a/src/model/session-setup.ts +++ b/src/model/session-setup.ts @@ -17,7 +17,10 @@ export interface SessionSetupInput { prompt: LanguageModelV3Prompt; options: LanguageModelV3CallOptions; streaming: boolean; - buildSessionConfig: (streaming: boolean) => Record; + buildSessionConfig: ( + streaming: boolean, + callOptions: LanguageModelV3CallOptions, + ) => Record; generateWarnings: (options: LanguageModelV3CallOptions) => SharedV3Warning[]; getClient: () => CopilotClient; systemMessageFromSettings?: SystemMessageConfig; @@ -65,7 +68,7 @@ export async function prepareSession(input: SessionSetupInput): Promise { + it("returns empty array when tools is undefined", () => { + expect(convertAiSdkToolsToCopilotTools(undefined)).toEqual([]); + }); + + it("returns empty array when tools is empty", () => { + expect(convertAiSdkToolsToCopilotTools([])).toEqual([]); + }); + + it("skips tools without providerOptions.github-copilot.execute", () => { + const tools = [ + { + type: "function" as const, + name: "weather", + description: "Get weather", + inputSchema: { type: "object", properties: { city: { type: "string" } } }, + }, + ]; + expect(convertAiSdkToolsToCopilotTools(tools)).toEqual([]); + }); + + it("skips provider tools (type provider)", () => { + const tools = [ + { + type: "provider" as const, + name: "mcp_tool", + id: "mcp.weather", + args: {}, + }, + ]; + expect(convertAiSdkToolsToCopilotTools(tools)).toEqual([]); + }); + + it("converts function tool with providerOptions.github-copilot.execute", () => { + const execute = async (args: { city: string }) => ({ city: args.city, temp: 22 }); + const tools = [ + { + type: "function" as const, + name: "get_weather", + description: "Get weather for a city", + inputSchema: { + type: "object", + properties: { city: { type: "string", description: "City name" } }, + required: ["city"], + }, + providerOptions: { + "github-copilot": { execute }, + }, + }, + ]; + + const result = convertAiSdkToolsToCopilotTools(tools); + expect(result).toHaveLength(1); + expect(result[0].name).toBe("get_weather"); + expect(result[0].description).toBe("Get weather for a city"); + expect(result[0].parameters).toEqual(tools[0].inputSchema); + expect(typeof result[0].handler).toBe("function"); + + const handlerResult = result[0].handler({ city: "Tokyo" }, {} as never); + return expect(Promise.resolve(handlerResult)).resolves.toEqual({ + city: "Tokyo", + temp: 22, + }); + }); + + it("skips tool when providerOptions.github-copilot has no execute", () => { + const tools = [ + { + type: "function" as const, + name: "weather", + description: "Get weather", + inputSchema: { type: "object" }, + providerOptions: { + "github-copilot": { otherKey: "value" }, + }, + }, + ]; + expect(convertAiSdkToolsToCopilotTools(tools)).toEqual([]); + }); +}); diff --git a/tests/model/session-setup.test.ts b/tests/model/session-setup.test.ts index b34c5df..6e6a344 100644 --- a/tests/model/session-setup.test.ts +++ b/tests/model/session-setup.test.ts @@ -27,9 +27,10 @@ describe("prepareSession", () => { const prompt: LanguageModelV3Prompt = [ { role: "user", content: [{ type: "text", text: "Hello" }] }, ]; + const options = { prompt }; const result = await prepareSession({ prompt, - options: { prompt }, + options, streaming: false, buildSessionConfig: () => ({ model: "gpt-4", streaming: false }), generateWarnings: () => [], @@ -41,25 +42,26 @@ describe("prepareSession", () => { }); it("merges buildSessionConfig with streaming flag", async () => { - const buildSessionConfig = vi.fn((streaming: boolean) => ({ + const prompt: LanguageModelV3Prompt = [ + { role: "user", content: [{ type: "text", text: "Hi" }] }, + ]; + const options = { prompt }; + const buildSessionConfig = vi.fn((_streaming: boolean, _callOptions: typeof options) => ({ model: "gpt-4", - streaming, + streaming: true, customKey: "value", })); - const prompt: LanguageModelV3Prompt = [ - { role: "user", content: [{ type: "text", text: "Hi" }] }, - ]; await prepareSession({ prompt, - options: { prompt }, + options, streaming: true, buildSessionConfig, generateWarnings: () => [], getClient: () => mockClient as never, }); - expect(buildSessionConfig).toHaveBeenCalledWith(true); + expect(buildSessionConfig).toHaveBeenCalledWith(true, options); expect(mockClient.createSession).toHaveBeenCalledWith( expect.objectContaining({ model: "gpt-4", From 003f4d4856cec70193bd8b0de7cc6645fa7a3f02 Mon Sep 17 00:00:00 2001 From: Mansur Nurmukhambetov Date: Sat, 31 Jan 2026 10:59:30 +0500 Subject: [PATCH 03/10] ci: update GitHub Actions workflow to trigger on pull request events for the main branch --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4adfedd..513237d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,8 @@ on: push: branches: ["main"] pull_request: + branches: ["main"] + types: [opened, synchronize, reopened] jobs: test: From 17928a854c928996096971054eed51d29dd542c6 Mon Sep 17 00:00:00 2001 From: Mansur Nurmukhambetov Date: Sat, 31 Jan 2026 11:07:19 +0500 Subject: [PATCH 04/10] docs: enhance README and tools example with additional usage scenarios and clarify tool configuration methods --- README.md | 15 ++++++++++++--- examples/tools.ts | 6 +++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 049ec2b..63538a8 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,15 @@ for await (const chunk of result.textStream) { Use model IDs available via Copilot CLI. Run `copilot -i /models` to list available models in your environment. +## Examples + +| Script | Description | +|--------|-------------| +| `npm run example:basic` | Non-streaming `generateText` | +| `npm run example:streaming` | Streaming `streamText` | +| `npm run example:tools` | Model-level tools via Copilot `defineTool` | +| `npm run example:tools-ai-sdk` | Call-level AI SDK `tool()` with `providerOptions` bridge | + ## Configuration ### Provider settings @@ -82,9 +91,9 @@ const model = githubCopilot("gpt-5", { ### Custom tools -You can pass tools in two ways: +Tools can be passed in two ways. When both are used, call-level tools are merged with model-level tools before creating the Copilot session. -**1. Copilot's `defineTool`** (model settings) — use when configuring the model: +**1. Copilot's `defineTool`** (model-level) — configure tools when creating the model: ```typescript import { defineTool } from "@github/copilot-sdk"; @@ -106,7 +115,7 @@ const model = githubCopilot("gpt-5", { }); ``` -**2. AI SDK `tool()` with `providerOptions`** (call-level tools) — pass `execute` via `providerOptions['github-copilot']` so the provider can use it as the Copilot handler: +**2. AI SDK `tool()` with `providerOptions`** (call-level) — the AI SDK does not pass `execute` to providers. Pass it via `providerOptions['github-copilot'].execute` so the provider can convert the tool and use it as the Copilot handler: ```typescript import { tool } from "ai"; diff --git a/examples/tools.ts b/examples/tools.ts index bbc68d7..28a0e96 100644 --- a/examples/tools.ts +++ b/examples/tools.ts @@ -1,8 +1,8 @@ /** - * Tools example - using tools with GitHub Copilot + * Tools example - model-level tools via Copilot defineTool * - * NOTE: AI SDK tools (tool() with Zod schemas and execute) are not yet supported. - * Use Copilot's defineTool with parameters and handler instead, as shown below. + * Use Copilot's defineTool when configuring the model. For call-level AI SDK + * tools (tool() with Zod schemas and execute), see examples/tools-ai-sdk.ts. * * Prerequisites: * - Copilot CLI installed and authenticated From afaa44b8cbced12b2aba7edc8208dbdcd6044d0f Mon Sep 17 00:00:00 2001 From: Mansur Nurmukhambetov Date: Sat, 31 Jan 2026 11:09:47 +0500 Subject: [PATCH 05/10] refactor: introduce copilotToolOptions for improved AI SDK tool integration - Replace direct execution function passing in providerOptions with copilotToolOptions for better clarity and usability. - Update README and example to reflect the new usage pattern for AI SDK tools with GitHub Copilot. - Add new copilot-tool-options module to encapsulate the provider options logic. --- README.md | 9 ++++---- examples/tools-ai-sdk.ts | 13 +++++------ src/index.ts | 2 ++ src/provider/copilot-tool-options.ts | 32 ++++++++++++++++++++++++++++ src/provider/index.ts | 2 ++ 5 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 src/provider/copilot-tool-options.ts diff --git a/README.md b/README.md index 63538a8..3f993ea 100644 --- a/README.md +++ b/README.md @@ -115,13 +115,12 @@ const model = githubCopilot("gpt-5", { }); ``` -**2. AI SDK `tool()` with `providerOptions`** (call-level) — the AI SDK does not pass `execute` to providers. Pass it via `providerOptions['github-copilot'].execute` so the provider can convert the tool and use it as the Copilot handler: +**2. AI SDK `tool()` with `providerOptions`** (call-level) — the AI SDK does not pass `execute` to providers. Use `copilotToolOptions(execute)` so the provider can convert the tool and use it as the Copilot handler: ```typescript -import { tool } from "ai"; +import { copilotToolOptions, githubCopilot } from "@nomomon/ai-sdk-provider-github-copilot"; +import { streamText, tool } from "ai"; import { z } from "zod"; -import { githubCopilot } from "@nomomon/ai-sdk-provider-github-copilot"; -import { streamText } from "ai"; const execute = async ({ city }: { city: string }) => ({ city, @@ -135,7 +134,7 @@ const getWeather = tool({ city: z.string().describe("The city name"), }), execute, - providerOptions: { "github-copilot": { execute } }, + providerOptions: copilotToolOptions(execute), }); const result = streamText({ diff --git a/examples/tools-ai-sdk.ts b/examples/tools-ai-sdk.ts index b9a00e8..0b85bc3 100644 --- a/examples/tools-ai-sdk.ts +++ b/examples/tools-ai-sdk.ts @@ -1,9 +1,10 @@ /** - * AI SDK tools example - using tool() with providerOptions bridge + * AI SDK tools example - call-level tools via tool() with providerOptions bridge * - * The AI SDK does not pass the tool's execute function to providers. - * Pass it via providerOptions['github-copilot'].execute so the provider - * can use it as the Copilot handler. + * The AI SDK does not pass the tool's execute function to providers. Pass it + * via providerOptions['github-copilot'].execute so the provider can convert + * the tool and use it as the Copilot handler. These call-level tools are + * merged with any model-level tools (defineTool) before creating the session. * * Prerequisites: * - Copilot CLI installed and authenticated @@ -11,7 +12,7 @@ * Run: npm run example:tools-ai-sdk */ -import { githubCopilot } from "@nomomon/ai-sdk-provider-github-copilot"; +import { copilotToolOptions, githubCopilot } from "@nomomon/ai-sdk-provider-github-copilot"; import { streamText, tool } from "ai"; import { z } from "zod"; @@ -31,7 +32,7 @@ async function main() { city: z.string().describe("The city name"), }), execute, - providerOptions: { "github-copilot": { execute } }, + providerOptions: copilotToolOptions(execute), }); const result = streamText({ diff --git a/src/index.ts b/src/index.ts index df25e4a..5d7bf8c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,8 @@ export { isAuthenticationError, } from "./errors.js"; export { GitHubCopilotLanguageModel } from "./model/github-copilot-language-model.js"; +export type { CopilotToolExecute } from "./provider/copilot-tool-options.js"; +export { copilotToolOptions } from "./provider/copilot-tool-options.js"; export type { GitHubCopilotModelId, GitHubCopilotProvider, diff --git a/src/provider/copilot-tool-options.ts b/src/provider/copilot-tool-options.ts new file mode 100644 index 0000000..f412037 --- /dev/null +++ b/src/provider/copilot-tool-options.ts @@ -0,0 +1,32 @@ +import type { ProviderOptions } from "@ai-sdk/provider-utils"; + +/** + * Execute handler for AI SDK tools when used with the GitHub Copilot provider. + * The AI SDK does not pass the tool's `execute` function to providers, so it + * must be passed via providerOptions for the provider to convert and use it. + */ +export type CopilotToolExecute = (args: unknown) => unknown | Promise; + +/** + * Creates provider options for AI SDK tools to work with the GitHub Copilot provider. + * + * The AI SDK does not pass the tool's `execute` function to providers. This helper + * bridges that gap by placing the execute handler in providerOptions so the provider + * can convert the tool and use it as the Copilot handler. + * + * @example + * ```ts + * const execute = async ({ city }: { city: string }) => ({ ... }); + * const getWeather = tool({ + * description: "Get the current weather for a city", + * inputSchema: z.object({ city: z.string() }), + * execute, + * providerOptions: copilotToolOptions(execute), + * }); + * ``` + */ +export function copilotToolOptions( + execute: (args: INPUT) => unknown | Promise, +): ProviderOptions { + return { "github-copilot": { execute } } as unknown as ProviderOptions; +} diff --git a/src/provider/index.ts b/src/provider/index.ts index e4dd9d5..46f13de 100644 --- a/src/provider/index.ts +++ b/src/provider/index.ts @@ -1,3 +1,5 @@ +export type { CopilotToolExecute } from "./copilot-tool-options.js"; +export { copilotToolOptions } from "./copilot-tool-options.js"; export type { GitHubCopilotModelId, GitHubCopilotProvider } from "./github-copilot-provider.js"; export { createGitHubCopilot, githubCopilot } from "./github-copilot-provider.js"; export type { GitHubCopilotProviderOptions, GitHubCopilotSettings } from "./types.js"; From 2e55b8bc3e9374ebd7865e621948f94f7e3b4dc8 Mon Sep 17 00:00:00 2001 From: Mansur Nurmukhambetov Date: Sat, 31 Jan 2026 11:12:48 +0500 Subject: [PATCH 06/10] test: enhance GitHubCopilotLanguageModel tests and add copilotToolOptions tests - Refactor existing tests for GitHubCopilotLanguageModel to improve readability and structure. - Add new tests for copilotToolOptions to verify provider options and execute handler functionality. - Ensure comprehensive coverage of model properties and behavior in the tests. --- .../github-copilot-language-model.test.ts | 125 ++++++++++++++++++ tests/provider/copilot-tool-options.test.ts | 28 ++++ 2 files changed, 153 insertions(+) create mode 100644 tests/provider/copilot-tool-options.test.ts diff --git a/tests/model/github-copilot-language-model.test.ts b/tests/model/github-copilot-language-model.test.ts index 908da27..9711d55 100644 --- a/tests/model/github-copilot-language-model.test.ts +++ b/tests/model/github-copilot-language-model.test.ts @@ -1,4 +1,6 @@ +import type { LanguageModelV3CallOptions } from "@ai-sdk/provider"; import type { CopilotClient } from "@github/copilot-sdk"; +import { defineTool } from "@github/copilot-sdk"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { GitHubCopilotLanguageModel } from "@/model/github-copilot-language-model.js"; @@ -188,6 +190,129 @@ describe("GitHubCopilotLanguageModel", () => { }); }); + describe("tools merging", () => { + it("converts call-level AI SDK tools with providerOptions and passes to createSession", async () => { + const execute = async (args: { city: string }) => ({ city: args.city, temp: 22 }); + mockSession.sendAndWait.mockResolvedValue({ data: {} }); + + const model = new GitHubCopilotLanguageModel({ + modelId: "gpt-4", + settings: {}, + getClient, + }); + + await model.doGenerate({ + prompt: [{ role: "user", content: [{ type: "text", text: "Weather in Tokyo?" }] }], + tools: [ + { + type: "function", + name: "get_weather", + description: "Get weather for a city", + inputSchema: { + type: "object", + properties: { city: { type: "string", description: "City name" } }, + required: ["city"], + }, + providerOptions: { "github-copilot": { execute } }, + }, + ], + } as unknown as LanguageModelV3CallOptions); + + const createSessionCall = mockClient.createSession.mock.calls[0][0]; + expect(createSessionCall.tools).toBeDefined(); + expect(createSessionCall.tools).toHaveLength(1); + expect(createSessionCall.tools[0].name).toBe("get_weather"); + expect(createSessionCall.tools[0].description).toBe("Get weather for a city"); + }); + + it("passes model-level tools to createSession when no call-level tools", async () => { + const modelTool = defineTool("lookup_issue", { + description: "Look up an issue", + parameters: { type: "object", properties: { id: { type: "string" } } }, + handler: async () => ({}), + }); + mockSession.sendAndWait.mockResolvedValue({ data: {} }); + + const model = new GitHubCopilotLanguageModel({ + modelId: "gpt-4", + settings: { tools: [modelTool] }, + getClient, + }); + + await model.doGenerate({ + prompt: [{ role: "user", content: [{ type: "text", text: "Hi" }] }], + }); + + const createSessionCall = mockClient.createSession.mock.calls[0][0]; + expect(createSessionCall.tools).toEqual([modelTool]); + }); + + it("merges model-level and call-level tools (model first, then AI SDK)", async () => { + const modelTool = defineTool("model_tool", { + description: "Model-level tool", + parameters: { type: "object" }, + handler: async () => ({}), + }); + const execute = async () => ({}); + mockSession.sendAndWait.mockResolvedValue({ data: {} }); + + const model = new GitHubCopilotLanguageModel({ + modelId: "gpt-4", + settings: { tools: [modelTool] }, + getClient, + }); + + await model.doGenerate({ + prompt: [{ role: "user", content: [{ type: "text", text: "Hi" }] }], + tools: [ + { + type: "function", + name: "call_tool", + description: "Call-level tool", + inputSchema: { type: "object" }, + providerOptions: { "github-copilot": { execute } }, + }, + ], + } as unknown as LanguageModelV3CallOptions); + + const createSessionCall = mockClient.createSession.mock.calls[0][0]; + expect(createSessionCall.tools).toHaveLength(2); + expect(createSessionCall.tools[0]).toBe(modelTool); + expect(createSessionCall.tools[1].name).toBe("call_tool"); + }); + + it("passes tools to createSession when doStream with call-level tools", async () => { + const execute = async () => ({}); + mockSession.send.mockResolvedValue(undefined); + mockSession.on.mockImplementation((callback: (e: unknown) => void) => { + setTimeout(() => callback({ type: "session.idle" }), 0); + }); + + const model = new GitHubCopilotLanguageModel({ + modelId: "gpt-4", + settings: {}, + getClient, + }); + + await model.doStream({ + prompt: [{ role: "user", content: [{ type: "text", text: "Hi" }] }], + tools: [ + { + type: "function", + name: "stream_tool", + description: "Stream tool", + inputSchema: { type: "object" }, + providerOptions: { "github-copilot": { execute } }, + }, + ], + } as unknown as LanguageModelV3CallOptions); + + const createSessionCall = mockClient.createSession.mock.calls[0][0]; + expect(createSessionCall.tools).toHaveLength(1); + expect(createSessionCall.tools[0].name).toBe("stream_tool"); + }); + }); + describe("doStream", () => { it("returns stream and request", async () => { mockSession.send.mockResolvedValue(undefined); diff --git a/tests/provider/copilot-tool-options.test.ts b/tests/provider/copilot-tool-options.test.ts new file mode 100644 index 0000000..09b1c75 --- /dev/null +++ b/tests/provider/copilot-tool-options.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { copilotToolOptions } from "@/provider/copilot-tool-options.js"; + +describe("copilotToolOptions", () => { + it("returns provider options with github-copilot key and execute handler", () => { + const execute = async (args: { city: string }) => ({ city: args.city, temp: 22 }); + const result = copilotToolOptions(execute); + + expect(result).toEqual({ + "github-copilot": { execute }, + }); + }); + + it("preserves execute function reference for use in tool conversion", () => { + const execute = (args: unknown) => args; + const result = copilotToolOptions(execute); + + expect(result["github-copilot"]).toBeDefined(); + expect(result["github-copilot"].execute).toBe(execute); + }); + + it("works with sync execute handler", () => { + const execute = (args: { x: number }) => ({ doubled: args.x * 2 }); + const result = copilotToolOptions(execute); + + expect(result["github-copilot"].execute({ x: 5 })).toEqual({ doubled: 10 }); + }); +}); From 71df11395aeba15768e21d1c219afb743fd85cb3 Mon Sep 17 00:00:00 2001 From: Mansur Nurmukhambetov Date: Sat, 31 Jan 2026 11:16:37 +0500 Subject: [PATCH 07/10] fix: update tools type definition in GitHubCopilotSettings for better type safety - Change the type of tools in GitHubCopilotSettings to accept a more specific object structure, enhancing type safety and clarity in tool configurations. --- src/provider/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/provider/types.ts b/src/provider/types.ts index 4304340..cd683b9 100644 --- a/src/provider/types.ts +++ b/src/provider/types.ts @@ -31,7 +31,7 @@ export interface GitHubCopilotSettings { * Custom tools exposed to the Copilot CLI. * Use Copilot's defineTool; tool inputs are objects with string/number/nested values. */ - tools?: Tool[]; + tools?: Tool<{ [key: string]: string | number | boolean | object }>[]; /** * Custom provider configuration (BYOK - Bring Your Own Key). From d897baaa0e6eb7920d68c37944bbd2cdebb15ad6 Mon Sep 17 00:00:00 2001 From: Mansur Nurmukhambetov Date: Sat, 31 Jan 2026 11:22:28 +0500 Subject: [PATCH 08/10] fix: update tools type definition in GitHubCopilotSettings to use any for flexibility - Modify the tools property in GitHubCopilotSettings to accept any type, allowing for greater flexibility in tool configurations while maintaining clarity in the documentation. --- src/provider/types.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/provider/types.ts b/src/provider/types.ts index cd683b9..e07acee 100644 --- a/src/provider/types.ts +++ b/src/provider/types.ts @@ -29,9 +29,10 @@ export interface GitHubCopilotSettings { /** * Custom tools exposed to the Copilot CLI. - * Use Copilot's defineTool; tool inputs are objects with string/number/nested values. + * Use Copilot's defineTool. */ - tools?: Tool<{ [key: string]: string | number | boolean | object }>[]; + // @biome-ignore lint/suspicious/noExplicitAny: it's ok to use any here + tools?: Tool[]; /** * Custom provider configuration (BYOK - Bring Your Own Key). From 559a608402862e08aee4dd3dba19b566aef7d35b Mon Sep 17 00:00:00 2001 From: Mansur Nurmukhambetov Date: Sat, 31 Jan 2026 11:24:45 +0500 Subject: [PATCH 09/10] style: standardize indentation in GitHubCopilotSettings and GitHubCopilotProviderOptions - Adjust indentation for properties in GitHubCopilotSettings and GitHubCopilotProviderOptions to ensure consistent formatting and improve code readability. From 1cab98be5605b8dd0f78e123677428c030d62450 Mon Sep 17 00:00:00 2001 From: Mansur Nurmukhambetov Date: Sat, 31 Jan 2026 11:35:02 +0500 Subject: [PATCH 10/10] fix: update tools type definition in GitHubCopilotSettings for improved type safety - Change the tools property in GitHubCopilotSettings to use unknown instead of any, enhancing type safety while maintaining flexibility in tool configurations. - Update copilotToolOptions test to reflect the new type definition and ensure proper execution handling. --- src/provider/types.ts | 3 +-- tests/provider/copilot-tool-options.test.ts | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/provider/types.ts b/src/provider/types.ts index e07acee..250ae7f 100644 --- a/src/provider/types.ts +++ b/src/provider/types.ts @@ -31,8 +31,7 @@ export interface GitHubCopilotSettings { * Custom tools exposed to the Copilot CLI. * Use Copilot's defineTool. */ - // @biome-ignore lint/suspicious/noExplicitAny: it's ok to use any here - tools?: Tool[]; + tools?: Tool[]; /** * Custom provider configuration (BYOK - Bring Your Own Key). diff --git a/tests/provider/copilot-tool-options.test.ts b/tests/provider/copilot-tool-options.test.ts index 09b1c75..99b7d5d 100644 --- a/tests/provider/copilot-tool-options.test.ts +++ b/tests/provider/copilot-tool-options.test.ts @@ -23,6 +23,9 @@ describe("copilotToolOptions", () => { const execute = (args: { x: number }) => ({ doubled: args.x * 2 }); const result = copilotToolOptions(execute); - expect(result["github-copilot"].execute({ x: 5 })).toEqual({ doubled: 10 }); + const opts = result["github-copilot"] as unknown as { + execute: (args: { x: number }) => unknown; + }; + expect(opts.execute({ x: 5 })).toEqual({ doubled: 10 }); }); });