Skip to content
Draft
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
31 changes: 31 additions & 0 deletions packages/acpx-ai-provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,37 @@ const provider = createAcpxProvider({
> TCP-callback story from `acp-ai-provider`) are **not** supported in
> v0.1. See [Known limitations](#known-limitations).

## Listing models

Some agents (Claude Code, Codex) advertise the models they can drive
when the session opens. Use `getModels()` to read both the available
list and the currently selected id:

```ts
const models = await provider.getModels()
if (models) {
console.log(models.availableModelIds) // ['claude-haiku-4-5', …]
console.log(models.currentModelId) // 'claude-opus-4-7'
}
```

Returns `undefined` when:

- The agent didn't advertise any models (e.g. Gemini CLI, custom
adapters that omit `NewSessionResponse.models`).
- The underlying runtime doesn't implement `getStatus`.

`getModels()` lazily spawns the ACP session if it isn't already
open — same as `prepare()`. For multi-session providers, pass the
same `{ sessionKey, agent }` you'd pass to `languageModel()`:

```ts
const models = await provider.getModels({ sessionKey: 'codex::/repo' })
```

To **change** the model, use `setConfigOption('model', id)` from
[Lifecycle controls](#lifecycle-controls).

## Structured output (JSON)

`generateObject` / `streamObject` work via JSON mode. The provider
Expand Down
6 changes: 3 additions & 3 deletions packages/acpx-ai-provider/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "acpx-ai-provider",
"version": "0.0.1",
"version": "0.1.0",
"description": "Vercel AI SDK provider on top of acpx/runtime — bring any ACP agent (Claude, Codex, Gemini, Copilot, Cursor…) to AI SDK with one install.",
"license": "MIT",
"author": "Dani Akash",
Expand Down Expand Up @@ -48,13 +48,13 @@
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"acpx": ">=0.6.1",
"acpx": ">=0.8.0",
"ai": ">=6.0.0"
},
"devDependencies": {
"@ai-sdk/provider": "^3.0.10",
"@ai-sdk/provider-utils": "^4.0.26",
"acpx": "^0.6.1",
"acpx": ">=0.8.0",
"ai": "^6.0.175",
"bunup": "^0.16.31",
"typescript": "^6.0.3",
Expand Down
2 changes: 2 additions & 0 deletions packages/acpx-ai-provider/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export type {
AcpRuntimeDoctorReport,
AcpRuntimeEvent,
AcpRuntimeHandle,
AcpRuntimeSessionModels,
AcpRuntimeStatus,
AcpRuntimeTurnResult,
AcpRuntimeTurnResultError,
AcpxLanguageModelOptions,
Expand Down
11 changes: 11 additions & 0 deletions packages/acpx-ai-provider/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
AcpRuntimeDoctorReport,
AcpRuntimeHandle,
AcpRuntimeOptions,
AcpRuntimeSessionModels,
} from 'acpx/runtime'
import {
createAcpRuntime,
Expand Down Expand Up @@ -135,6 +136,16 @@ export class AcpxProvider {
}
}

async getModels(
opts: AcpxLanguageModelOptions = {},
): Promise<AcpRuntimeSessionModels | undefined> {
const getStatusImpl = this.runtime.getStatus
if (!getStatusImpl) return undefined
const { handle } = await this.ensureHandle(opts)
const status = await getStatusImpl.call(this.runtime, { handle })
return status.models
}

async doctor(): Promise<AcpRuntimeDoctorReport> {
const doctorImpl = this.runtime.doctor
if (!doctorImpl) {
Expand Down
4 changes: 4 additions & 0 deletions packages/acpx-ai-provider/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type {
AcpRuntimeDoctorReport,
AcpRuntimeEvent,
AcpRuntimeHandle,
AcpRuntimeSessionModels,
AcpRuntimeStatus,
AcpRuntimeTurnResult,
AcpRuntimeTurnResultError,
} from 'acpx/runtime'
Expand Down Expand Up @@ -58,6 +60,8 @@ export type {
AcpRuntimeDoctorReport,
AcpRuntimeEvent,
AcpRuntimeHandle,
AcpRuntimeSessionModels,
AcpRuntimeStatus,
AcpRuntimeTurnResult,
AcpRuntimeTurnResultError,
}
68 changes: 68 additions & 0 deletions packages/acpx-ai-provider/test/unit/provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, expect, test } from 'bun:test'
import type { AcpRuntime } from 'acpx/runtime'
import { createAcpxProvider } from '../../src/provider.ts'
import { MockAcpRuntime } from '../helpers/mock-acp-runtime.ts'

describe('AcpxProvider — getModels', () => {
test('returns the models field from runtime status', async () => {
const runtime = new MockAcpRuntime({
status: {
summary: 'mock',
models: {
currentModelId: 'claude-opus-4-7',
availableModelIds: [
'claude-haiku-4-5',
'claude-sonnet-4-6',
'claude-opus-4-7',
],
},
},
})
const provider = createAcpxProvider({ agent: 'claude', runtime })

const models = await provider.getModels()

expect(models).toEqual({
currentModelId: 'claude-opus-4-7',
availableModelIds: [
'claude-haiku-4-5',
'claude-sonnet-4-6',
'claude-opus-4-7',
],
})
expect(runtime.getStatusCalls).toHaveLength(1)
})

test('returns undefined when the runtime status omits models', async () => {
const runtime = new MockAcpRuntime({ status: { summary: 'no models' } })
const provider = createAcpxProvider({ agent: 'gemini', runtime })

const models = await provider.getModels()

expect(models).toBeUndefined()
expect(runtime.getStatusCalls).toHaveLength(1)
})

test('returns undefined when the runtime has no getStatus method', async () => {
const runtime: AcpRuntime = {
ensureSession: async () => ({
sessionKey: 'k',
backend: 'mock',
runtimeSessionName: 'mock',
}),
startTurn: () => {
throw new Error('not used')
},
runTurn: async function* () {
// unused
},
cancel: async () => {},
close: async () => {},
}
const provider = createAcpxProvider({ agent: 'custom', runtime })

const models = await provider.getModels()

expect(models).toBeUndefined()
})
})