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
9 changes: 8 additions & 1 deletion deploy/example.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { env } from 'cloudflare:workers'
import type { Config } from '@deploy/types'

// can be whatever you want, just used to make linking apiKeys to providers typesafe.
type ProviderKeys = 'openai' | 'groq' | 'google-vertex' | 'anthropic' | 'bedrock'
type ProviderKeys = 'openai' | 'anthropic' | 'google-vertex' | 'bedrock' | 'groq' | 'azure'

// projects, users and keys must have numeric keys, using constants here to make it easier to understand
// of course, keys must be unique within a type (e.g. project ids must be unique) but users and projects can have the same id
Expand Down Expand Up @@ -72,6 +72,13 @@ export const config: Config<ProviderKeys> = {
// credentials are used by the ProviderProxy to authenticate the forwarded request
credentials: env.OPENAI_API_KEY,
},
azure: {
providerId: 'azure',
// NOTE: For now, you need to specify the family of models you want to use.
baseUrl: 'https://marcelo-0665-resource.openai.azure.com/openai/v1',
injectCost: true,
credentials: env.AZURE_API_KEY,
},
groq: { providerId: 'groq', baseUrl: 'https://api.groq.com', injectCost: true, credentials: env.GROQ_API_KEY },
'google-vertex': {
providerId: 'google-vertex',
Expand Down
3 changes: 3 additions & 0 deletions deploy/example.env.local
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ LOGFIRE_TOKEN=pylf_v1_...
# if you want to use OpenAI, generate an API key (you would use env.OPENAI_API_KEY in config.ts)
OPENAI_API_KEY=...

# if you want to use Azure, generate an API key (you would use env.AZURE_API_KEY in config.ts)
AZURE_API_KEY=...

# same for Groq (again you would use env.GROQ_API_KEY in config.ts)
GROQ_API_KEY=...

Expand Down
32 changes: 32 additions & 0 deletions examples/ex_azure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import os

import logfire
from devtools import debug
from openai import OpenAI

logfire.configure()
logfire.instrument_httpx(capture_all=True)

api_key = os.getenv('PYDANTIC_AI_GATEWAY_API_KEY')
assert api_key is not None

client = OpenAI(api_key=api_key, base_url='http://localhost:8787/azure')

response = client.responses.create(
model='gpt-4.1',
instructions='reply concisely',
input='what color is the sky?',
)

print(response.output_text)
response.usage

completion = client.chat.completions.create(
model='gpt-4.1',
messages=[
{'role': 'developer', 'content': 'You are a helpful assistant.'},
{'role': 'user', 'content': 'what color is the sky?'},
],
)
debug(completion)
completion.usage
5 changes: 5 additions & 0 deletions gateway/src/providers/azure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { OpenAIProvider } from './openai'

// TODO(Marcelo): The `AzureProvider` should be its own class, not a subclass of `OpenAIProvider`.
export class AzureProvider extends OpenAIProvider {}
// TODO(Marcelo): We should support Anthropic models as well.
3 changes: 3 additions & 0 deletions gateway/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import type { ProviderID } from '../types'

import { AnthropicProvider } from './anthropic'
import { AzureProvider } from './azure'
import { BedrockProvider } from './bedrock'
import { DefaultProviderProxy, type ProviderOptions } from './default'
import { GoogleVertexProvider } from './google'
Expand All @@ -31,6 +32,8 @@ export function getProvider(providerId: ProviderID): ProviderSig {
switch (providerId) {
case 'openai':
return OpenAIProvider
case 'azure':
return AzureProvider
case 'groq':
return GroqProvider
case 'google-vertex':
Expand Down
5 changes: 3 additions & 2 deletions gateway/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export interface ApiKeyInfo<ProviderKey extends string = string> {
otelSettings?: OtelSettings
}

export type ProviderID = 'groq' | 'openai' | 'google-vertex' | 'anthropic' | 'test' | 'bedrock'
// TODO | 'azure' | 'fireworks' | 'mistral' | 'cohere'
export type ProviderID = 'groq' | 'openai' | 'google-vertex' | 'anthropic' | 'test' | 'bedrock' | 'azure'
// TODO | 'fireworks' | 'mistral' | 'cohere'

const providerIds: Record<ProviderID, boolean> = {
groq: true,
Expand All @@ -48,6 +48,7 @@ const providerIds: Record<ProviderID, boolean> = {
anthropic: true,
test: true,
bedrock: true,
azure: true,
}

export const providerIdsArray = Object.keys(providerIds) as ProviderID[]
Expand Down
1 change: 1 addition & 0 deletions gateway/test/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ interface Env {
KV: KVNamespace
GITHUB_SHA: string
limitsDB: D1Database
AZURE_API_KEY: string
OPENAI_API_KEY: string
GROQ_API_KEY: string
ANTHROPIC_API_KEY: string
Expand Down
2 changes: 1 addition & 1 deletion gateway/test/gateway.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('invalid request', () => {
const text = await response.text()
expect(response.status, `got ${response.status} response: ${text}`).toBe(404)
expect(text).toMatchInlineSnapshot(
`"Route not found: wrong. Supported values: anthropic, bedrock, converse, gemini, google-vertex, groq, openai, test"`,
`"Route not found: wrong. Supported values: anthropic, azure, bedrock, converse, gemini, google-vertex, groq, openai, test"`,
)
})
})
Expand Down
40 changes: 40 additions & 0 deletions gateway/test/providers/azure.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import OpenAI from 'openai'
import { describe, expect } from 'vitest'
import { deserializeRequest } from '../otel'
import { test } from '../setup'

describe('azure', () => {
test('chat', async ({ gateway }) => {
const { fetch, otelBatch } = gateway

const client = new OpenAI({ apiKey: 'healthy', baseURL: 'https://example.com/azure', fetch })

const completion = await client.chat.completions.create({
model: 'gpt-4.1',
messages: [
{ role: 'developer', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'What is the capital of France?' },
],
max_completion_tokens: 1024,
})

expect(completion).toMatchSnapshot('llm')
expect(otelBatch, 'otelBatch length not 1').toHaveLength(1)
expect(deserializeRequest(otelBatch[0]!)).toMatchSnapshot('span')
})

test('responses', async ({ gateway }) => {
const { fetch, otelBatch } = gateway

const client = new OpenAI({ apiKey: 'healthy', baseURL: 'https://example.com/azure', fetch })

const completion = await client.responses.create({
model: 'gpt-4.1',
instructions: 'reply concisely',
input: 'what color is the sky?',
})
expect(completion).toMatchSnapshot('llm')
expect(otelBatch, 'otelBatch length not 1').toHaveLength(1)
expect(deserializeRequest(otelBatch[0]!)).toMatchSnapshot('span')
})
})
Loading