Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
types: [opened, synchronize, reopened]

jobs:
test:
Expand Down
45 changes: 42 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -82,10 +91,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.
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.

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-level) — configure tools when creating the model:

```typescript
import { defineTool } from "@github/copilot-sdk";
Expand All @@ -107,6 +115,37 @@ const model = githubCopilot("gpt-5", {
});
```

**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 { copilotToolOptions, githubCopilot } from "@nomomon/ai-sdk-provider-github-copilot";
import { streamText, tool } from "ai";
import { z } from "zod";

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: copilotToolOptions(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.
Expand Down
54 changes: 54 additions & 0 deletions examples/tools-ai-sdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* 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 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
*
* Run: npm run example:tools-ai-sdk
*/

import { copilotToolOptions, 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: copilotToolOptions(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);
});
5 changes: 4 additions & 1 deletion examples/tools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/**
* Tools example - using tools with GitHub Copilot
* Tools example - model-level tools via Copilot defineTool
*
* 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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
67 changes: 67 additions & 0 deletions src/conversion/convert-ai-sdk-tools-to-copilot.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>;
};

function hasExecute(
opts: unknown,
): opts is CopilotProviderOptions & { execute: (args: unknown) => unknown | Promise<unknown> } {
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<LanguageModelV3FunctionTool | { type: string; name: string }> | undefined,
): Tool<unknown>[] {
if (!tools?.length) return [];

const result: Tool<unknown>[] = [];

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<string, unknown>,
handler: async (args: unknown) => execute(args),
});

result.push(copilotTool);
}

return result;
}
1 change: 1 addition & 0 deletions src/conversion/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 10 additions & 3 deletions src/model/github-copilot-language-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions src/model/session-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export interface SessionSetupInput {
prompt: LanguageModelV3Prompt;
options: LanguageModelV3CallOptions;
streaming: boolean;
buildSessionConfig: (streaming: boolean) => Record<string, unknown>;
buildSessionConfig: (
streaming: boolean,
callOptions: LanguageModelV3CallOptions,
) => Record<string, unknown>;
generateWarnings: (options: LanguageModelV3CallOptions) => SharedV3Warning[];
getClient: () => CopilotClient;
systemMessageFromSettings?: SystemMessageConfig;
Expand Down Expand Up @@ -65,7 +68,7 @@ export async function prepareSession(input: SessionSetupInput): Promise<SessionS
}

const session = await client.createSession({
...buildSessionConfig(streaming),
...buildSessionConfig(streaming, options),
systemMessage: systemMessage
? { mode: "append", content: systemMessage }
: systemMessageFromSettings,
Expand Down
32 changes: 32 additions & 0 deletions src/provider/copilot-tool-options.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>;

/**
* 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<INPUT>(
execute: (args: INPUT) => unknown | Promise<unknown>,
): ProviderOptions {
return { "github-copilot": { execute } } as unknown as ProviderOptions;
}
2 changes: 2 additions & 0 deletions src/provider/index.ts
Original file line number Diff line number Diff line change
@@ -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";
1 change: 1 addition & 0 deletions src/provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface GitHubCopilotSettings {

/**
* Custom tools exposed to the Copilot CLI.
* Use Copilot's defineTool.
*/
tools?: Tool<unknown>[];

Expand Down
Loading