Skip to content

Custom Providers (SDK)

Custom providers let you implement evaluation targets in TypeScript instead of shelling out to a CLI command. This is useful when you want to call an HTTP API, use an SDK, or implement custom logic that goes beyond what the CLI provider supports.

Every provider must implement the Provider interface from @agentv/core:

interface Provider {
readonly id: string;
readonly kind: string;
readonly targetName: string;
invoke(request: ProviderRequest): Promise<ProviderResponse>;
}

The request object passed to invoke():

FieldTypeDescription
questionstringThe input prompt from the eval case
systemPromptstring?Optional system prompt
guidelinesstring?Evaluation guidelines
inputFilesstring[]?File paths attached to the eval case
evalCaseIdstring?Unique identifier for this eval case
attemptnumber?Retry attempt number (0-based)
signalAbortSignal?Cancellation signal
cwdstring?Working directory override

The response object returned from invoke():

FieldTypeDescription
outputMessage[]?Output messages from the provider
tokenUsage{ input, output, cached? }?Token usage metrics
costUsdnumber?Total cost in USD
durationMsnumber?Execution duration in milliseconds
rawunknown?Raw provider-specific data for debugging

Each Message in the output array has:

FieldTypeDescription
rolestringMessage role (e.g., 'assistant')
contentunknown?Message content (usually a string)
toolCallsToolCall[]?Tool calls made in this message
durationMsnumber?Duration of this message in milliseconds

Use createBuiltinProviderRegistry() to get a registry pre-loaded with all built-in providers, then call .register() to add your own:

import {
createBuiltinProviderRegistry,
type ProviderFactoryFn,
type ResolvedTarget,
type Provider,
type ProviderRequest,
type ProviderResponse,
} from '@agentv/core';
const registry = createBuiltinProviderRegistry();
registry.register('my-provider', (target: ResolvedTarget): Provider => {
return {
id: `my-provider:${target.name}`,
kind: 'cli', // use 'cli' as the kind for custom providers
targetName: target.name,
async invoke(request: ProviderRequest): Promise<ProviderResponse> {
// Your implementation here
return {
output: [{ role: 'assistant', content: 'Hello from my provider' }],
};
},
};
});

The register() method takes two arguments:

  1. kind (string) — A unique identifier for your provider. This is the value used in provider: in targets.yaml.
  2. factory (ProviderFactoryFn) — A function that receives a ResolvedTarget and returns a Provider instance.

The factory function signature:

type ProviderFactoryFn = (target: ResolvedTarget) => Provider;

Here is a practical example that wraps a REST API as a custom provider:

import {
createBuiltinProviderRegistry,
type Provider,
type ProviderRequest,
type ProviderResponse,
type ResolvedTarget,
} from '@agentv/core';
class HttpAgentProvider implements Provider {
readonly id: string;
readonly kind = 'cli' as const;
readonly targetName: string;
private readonly baseUrl: string;
private readonly apiKey: string;
constructor(targetName: string, config: { baseUrl: string; apiKey: string }) {
this.id = `http-agent:${targetName}`;
this.targetName = targetName;
this.baseUrl = config.baseUrl;
this.apiKey = config.apiKey;
}
async invoke(request: ProviderRequest): Promise<ProviderResponse> {
const startTime = Date.now();
const response = await fetch(`${this.baseUrl}/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
prompt: request.question,
system: request.systemPrompt,
}),
signal: request.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const data = await response.json();
const durationMs = Date.now() - startTime;
return {
output: [{ role: 'assistant', content: data.text }],
tokenUsage: data.usage
? { input: data.usage.prompt_tokens, output: data.usage.completion_tokens }
: undefined,
costUsd: data.cost,
durationMs,
raw: data,
};
}
}
// Register the provider
const registry = createBuiltinProviderRegistry();
registry.register('http-agent', (target: ResolvedTarget) => {
const config = target.config as { baseUrl: string; apiKey: string };
return new HttpAgentProvider(target.name, {
baseUrl: config.baseUrl ?? 'http://localhost:8080',
apiKey: config.apiKey ?? '',
});
});

Then reference it in your targets file:

.agentv/targets.yaml
targets:
- name: my_http_agent
provider: http-agent
judge_target: azure_base

AgentV supports two approaches for custom targets:

AspectCLI ProviderNative TypeScript Provider
ConfigurationYAML only (provider: cli)TypeScript code + YAML
CommunicationShell command + JSON output fileDirect function call
Best forWrapping existing scripts, polyglot toolsHTTP APIs, SDKs, complex orchestration
SetupNo code requiredRequires a TypeScript entry point
DebuggingInspect output filesStandard TypeScript debugging
Token usageMust be included in JSON outputReturned directly in ProviderResponse

Use provider: cli when:

  • You have an existing script or binary to wrap
  • The agent is written in a different language (Python, Go, etc.)
  • You want zero TypeScript code
targets:
- name: python_agent
provider: cli
command: 'python agent.py --prompt-file {PROMPT_FILE} --output {OUTPUT_FILE}'

Use a custom TypeScript provider when:

  • You are calling an HTTP API or SDK directly
  • You need structured error handling or retry logic
  • You want to report token usage and cost programmatically
  • You need to share state across invocations (connection pools, auth tokens)