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
51 changes: 24 additions & 27 deletions app/_how-tos/ai-gateway/use-codex-with-ai-gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,9 @@ tldr:

prereqs:
inline:
- title: OpenAI
- title: OpenAI API key
include_content: md/ai-gateway/v2/prereqs/openai-kongctl
icon_url: /assets/icons/openai.svg
content: |
Get an API key from [platform.openai.com/api-keys](https://platform.openai.com/api-keys) and export it as the **full `Authorization` header value** (including the `Bearer ` prefix):

```sh
export OPENAI_AUTH_HEADER="Bearer your_api_key"
```
- title: Codex CLI
icon_url: /assets/icons/openai.svg
content: |
Expand Down Expand Up @@ -78,8 +73,13 @@ ai_gateway_models:
enabled: true
formats: [{ type: openai }]
config:
route: { paths: [/codex], methods: [GET, POST], model: { body_param: model, values: [gpt-5.4] } }
model: { name_header: true }
route:
paths:
- /
model:
body_param: model
values:
- codex-openai
capabilities: [agentic]
targets:
- name: gpt-5.4
Expand All @@ -98,29 +98,26 @@ In this example, we're setting up the AI Model with:

* `capabilities: [agentic]`: Routes requests to the OpenAI Responses API, which the Codex CLI uses.
* `formats: [{ type: openai }]`: Accepts OpenAI-format requests.
* `config.route.model: { body_param: model, values: [gpt-5.4] }`: The model name the Codex CLI sends in each request.
* `route.paths: [/codex]`: The base path Codex points at; the Responses API is served at `/codex/responses`.

{% warning %}
If you are a new Codex user, you must Initialise the tool first by running `codex` and following the steps provided.
{% endwarning %}

## Point Codex CLI at {{site.ai_gateway}}

Open a new terminal and set `OPENAI_BASE_URL` to the local {{site.ai_gateway}} endpoint. The Codex CLI requires `OPENAI_API_KEY` to be set even though the real key lives on the gateway, so a placeholder is fine:

{% env_variables %}
OPENAI_API_KEY: sk-placeholder
OPENAI_BASE_URL: http://localhost:8000/codex
{% endenv_variables %}
* `config.route.model: { body_param: model, values: [codex-openai] }`: The model name the Codex CLI sends in each request.
* `route.paths: [/]`: The base path Codex points at; the Responses API is served at `/responses`.

## Start and use Codex CLI

Run a simple command to confirm traffic flows through {{site.ai_gateway}} to OpenAI:

<!--vale off-->
{% validation codex %}
model: gpt-5.4
prompt: Hello
model: codex-openai
model_provider: my-gateway
model_provider_name: AI Quickstart
model_provider_base_url: http://localhost:8000/
model_provider_env_key: OPENAI_API_KEY
model_provider_wire_api: responses
prompt: Tell me about the Madrid Skylitzes manuscript.
{% endvalidation %}
<!--vale on-->

When prompted for network access, select **Yes, proceed**. Codex routes the request through {{site.ai_gateway}} to the OpenAI Responses API and returns the model's response, giving you monitoring and control over all Codex LLM traffic.

When prompted for network access, select **Yes, proceed**. Codex routes the request through {{site.ai_gateway}} to the OpenAI Responses API and returns the model's response, giving you monitoring and control over all Codex LLM traffic.
{:.info}
> The Codex CLI requires `OPENAI_API_KEY` to be set even though the real key lives on the {{site.ai_gateway}}, so setting a placeholder is fine. You may be prompted to confirm this in the TUI interface.
Original file line number Diff line number Diff line change
Expand Up @@ -95,23 +95,20 @@ This example uses the following settings:
* `targets`: Sends requests to `gpt-5-mini` through the `generic-openai` provider.
* `capabilities: [generate]`: Exposes the model at a `/qwen/chat/completions` endpoint.

## Point Qwen Code CLI at {{site.ai_gateway}}

Open a new terminal and set `OPENAI_BASE_URL` to the local {{site.ai_gateway}} endpoint. Qwen Code CLI requires `OPENAI_API_KEY` to be set even though the real key lives on the gateway, so a placeholder is fine:

{% env_variables %}
OPENAI_API_KEY: sk-placeholder
OPENAI_BASE_URL: http://localhost:8000/qwen/chat/completions
{% endenv_variables %}

## Run Qwen Code CLI

Run Qwen Code CLI against the model configured in the AI Model entity's `targets`:

<!--vale off-->
{% validation qwen %}
base_url: http://localhost:8000/qwen/chat/completions
model: my-qwen-openai
auth-type: openai
prompt: Explain the singleton pattern in Python.
{% endvalidation %}
<!--vale on-->

Qwen Code CLI returns a response, proxied through {{site.ai_gateway}} to the OpenAI model.

{:.info}
> The Qwen Code CLI requires `OPENAI_API_KEY` to be set even though the real key lives on the {{site.ai_gateway}}.
18 changes: 8 additions & 10 deletions app/_how-tos/ai-gateway/use-qwen-code-with-ai-gateway-qwen.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,23 +105,21 @@ This example uses the following settings:
* `targets[0].config.international: true`: Uses DashScope's international endpoint (`dashscope-intl.aliyuncs.com`); set it to `false` if your DashScope key belongs to a mainland China account.
* `capabilities: [generate]`: Exposes the model at a `/qwen/chat/completions` endpoint.

## Point Qwen Code CLI at {{site.ai_gateway}}

Open a new terminal and set `OPENAI_BASE_URL` to the local {{site.ai_gateway}} endpoint. Qwen Code CLI requires `OPENAI_API_KEY` to be set even though the real key lives on the gateway, so a placeholder is fine:

{% env_variables %}
OPENAI_API_KEY: sk-placeholder
OPENAI_BASE_URL: http://localhost:8000/qwen-dashscope/chat/completions
{% endenv_variables %}

## Run Qwen Code CLI

Run Qwen Code CLI against the model configured in the AI Model entity's `targets`:

<!--vale off-->
{% validation qwen %}
open_api_key: sk-placeholder
base_url: http://localhost:8000/qwen-dashscope/chat/completions
model: my-qwen-dashscope
auth-type: openai
prompt: Explain the singleton pattern in Python.
{% endvalidation %}
<!--vale on-->

Qwen Code CLI returns a response, proxied through {{site.ai_gateway}} to the upstream DashScope Qwen model.

Qwen Code CLI returns a response, proxied through {{site.ai_gateway}} to the upstream DashScope Qwen model.
{:.info}
> The Qwen Code CLI requires `OPENAI_API_KEY` to be set even though the real key lives on the {{site.ai_gateway}}, so setting a placeholder is fine.
8 changes: 3 additions & 5 deletions app/_includes/how-tos/validations/codex/snippet.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
```sh
{% if include.config.base_url -%}
export OPENAI_BASE_URL={{include.config.base_url}}

{% endif -%}
{{include.config.base_command}}
{{include.config.base_command}} \
{%- for flag in include.config.flags %}
--config {{flag}}{%- unless forloop.last %} \{% endunless %}{%- endfor %}
```

And ask a question to confirm that requests reach {{site.ai_gateway}}.
Expand Down
14 changes: 14 additions & 0 deletions app/_includes/how-tos/validations/qwen/snippet.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
{%- capture env_exports -%}
{% if include.config.open_api_key -%}
export OPENAI_API_KEY={{include.config.open_api_key}}
{% endif -%}
{% if include.config.base_url -%}
export OPENAI_BASE_URL={{include.config.base_url}}
{% endif -%}
{%- endcapture -%}

```sh
{% if env_exports != empty -%}
{{ env_exports | strip }}

{% endif -%}

{{include.config.base_command}}
```

Expand Down
63 changes: 49 additions & 14 deletions app/_plugins/drops/validations/codex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,77 @@ module Jekyll
module Drops
module Validations
class Codex < Base # rubocop:disable Style/Documentation
REQUIRED_FIELDS = %w[
prompt model model_provider model_provider_name
model_provider_base_url model_provider_env_key model_provider_wire_api
].freeze

def validate_yaml!
raise ArgumentError, "Missing `prompt` in {% validation #{id} %}." unless @yaml.key?('prompt')
raise ArgumentError, "Missing `model` in {% validation #{id} %}." unless @yaml.key?('model')
REQUIRED_FIELDS.each do |field|
raise ArgumentError, "Missing `#{field}` in {% validation #{id} %}." unless @yaml.key?(field)
end
end

def data_validate
JSON.dump({ name: id, config: config })
end

def config
@config ||= configuration.merge('command' => command, 'base_command' => base_command)
@config ||= configuration.merge('command' => command, 'base_command' => base_command, 'flags' => flags)
end

def base_command
@base_command ||= [
configuration.fetch('command'),
"--model \"#{@yaml.fetch('model')}\""
].join(' ')
@base_command ||= configuration.fetch('command')
end

def command
@command ||= [
*env_vars,
configuration.fetch('command'),
"exec \"#{@yaml.fetch('prompt')}\"",
"--model \"#{@yaml.fetch('model')}\"",
"exec \"#{prompt}\"",
*flags.map { |flag| "-c #{flag}" },
'--skip-git-repo-check'
].join(' ')
end

def flags
[
"model=\"#{model}\"",
"model_provider=\"#{provider}\"",
"model_providers.#{provider}.name=\"#{name}\"",
"model_providers.#{provider}.base_url=\"#{base_url}\"",
"model_providers.#{provider}.env_key=\"#{env_key}\"",
"model_providers.#{provider}.wire_api=\"#{wire_api}\""
]
end

private

def env_vars
[
("OPENAI_BASE_URL=#{self['base_url']}" if self['base_url'])
].compact
def prompt
@prompt ||= @yaml.fetch('prompt')
end

def model
@model ||= @yaml.fetch('model')
end

def provider
@provider ||= @yaml.fetch('model_provider')
end

def name
@name ||= @yaml.fetch('model_provider_name')
end

def base_url
@base_url ||= @yaml.fetch('model_provider_base_url')
end

def env_key
@env_key ||= @yaml.fetch('model_provider_env_key')
end

def wire_api
@wire_api ||= @yaml.fetch('model_provider_wire_api')
end
end
end
Expand Down
10 changes: 10 additions & 0 deletions app/_plugins/drops/validations/qwen.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,22 @@ def base_command

def command
@command ||= [
*env_vars,
configuration.fetch('command'),
"--model \"#{@yaml.fetch('model')}\"",
"--auth-type \"#{@yaml.fetch('auth-type')}\"",
"--prompt \"#{@yaml.fetch('prompt')}\""
].join(' ')
end

private

def env_vars
[
("OPENAI_API_KEY=#{self['open_api_key']}" if self['open_api_key']),
("OPENAI_BASE_URL=#{self['base_url']}" if self['base_url'])
].compact
end
end
end
end
Expand Down
79 changes: 31 additions & 48 deletions spec/app/_includes/how-tos/validations/codex/snippet_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# frozen_string_literal: true

# The base case (no base_url) is adapted from the real {% validation codex %} block in
# app/_how-tos/ai-gateway/use-codex-with-ai-gateway.md. That's the only real-doc usage of
# this validation, and it never sets `base_url`, so the base_url branch is synthetic, built
# directly from the snippet.md logic, to get full branch coverage.
RSpec.describe 'how-tos/validations/codex/snippet.md' do
let(:validations_config) do
[{ 'id' => 'codex', 'command' => 'codex', 'expected' => { 'return_code' => 0 } }]
Expand All @@ -13,57 +9,44 @@

before { allow(Jekyll).to receive(:sites).and_return([site]) }

let(:yaml) do
{
'model' => 'codex-openai',
'model_provider' => 'my-gateway',
'model_provider_name' => 'AI Quickstart',
'model_provider_base_url' => 'http://localhost:8000/',
'model_provider_env_key' => 'OPENAI_API_KEY',
'model_provider_wire_api' => 'responses',
'prompt' => 'Tell me about the Madrid Skylitzes manuscript.'
}
end
let(:config) { Jekyll::Drops::Validations::Codex.new(id: 'codex', yaml: yaml) }
let(:template) { '{% include how-tos/validations/codex/snippet.md config=config %}' }

subject(:rendered) { render_liquid(template, locals: { 'config' => config }) }
let(:code) { bash_code_block(rendered, lang: 'sh') }

shared_examples 'a valid shell command' do
it 'renders syntactically valid shell' do
validate_bash_syntax!(code)
end
end

context 'with no base_url (use-codex-with-ai-gateway.md)' do
let(:yaml) { { 'model' => 'gpt-5.4', 'prompt' => 'Hello' } }

include_examples 'a valid shell command'

it 'renders the base command with no env export and no leading blank line' do
expect(rendered).to eq(<<~MD)
```sh
codex --model "gpt-5.4"
```

And ask a question to confirm that requests reach AI Gateway.

```text
Hello
```
MD
end
it 'renders syntactically valid shell' do
validate_bash_syntax!(code)
end

context 'with base_url (synthetic)' do
let(:yaml) { { 'model' => 'gpt-5.4', 'prompt' => 'Hello', 'base_url' => 'http://localhost:9000/codex' } }

include_examples 'a valid shell command'

it 'renders the OPENAI_BASE_URL export before the base command' do
expect(rendered).to eq(<<~MD)
```sh
export OPENAI_BASE_URL=http://localhost:9000/codex

codex --model "gpt-5.4"
```

And ask a question to confirm that requests reach AI Gateway.

```text
Hello
```
MD
end
it 'renders the base command with a --config flag per model provider setting' do
expect(rendered).to eq(<<~MD)
```sh
codex \\
--config model="codex-openai" \\
--config model_provider="my-gateway" \\
--config model_providers.my-gateway.name="AI Quickstart" \\
--config model_providers.my-gateway.base_url="http://localhost:8000/" \\
--config model_providers.my-gateway.env_key="OPENAI_API_KEY" \\
--config model_providers.my-gateway.wire_api="responses"
```

And ask a question to confirm that requests reach AI Gateway.

```text
Tell me about the Madrid Skylitzes manuscript.
```
MD
end
end
Loading
Loading