Skip to content

Commit 16f21be

Browse files
committed
fix(gateway): support Kilo chat completion streams
1 parent e819ac1 commit 16f21be

9 files changed

Lines changed: 293 additions & 30 deletions

File tree

.github/assets/kilo-favicon.png

1.2 KB
Loading

README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,72 @@ operator MCP token.
527527

528528
</details>
529529

530+
<details>
531+
<summary><img src=".github/assets/kilo-favicon.png" alt="Kilo logo" width="16" height="16"> Kilo <code>~/.config/kilo/kilo.jsonc</code></summary>
532+
533+
Kilo Code uses the OpenAI-compatible provider path for Codex Pooler's `/v1`
534+
surface. Install the current CLI from npm:
535+
536+
```bash
537+
npm install -g @kilocode/cli
538+
```
539+
540+
Then configure the provider in `~/.config/kilo/kilo.jsonc`:
541+
542+
```jsonc
543+
{
544+
"$schema": "https://app.kilo.ai/config.json",
545+
"model": "openai-compatible/gpt-5.5",
546+
"enabled_providers": ["openai-compatible"],
547+
"provider": {
548+
"openai-compatible": {
549+
"options": {
550+
"apiKey": "{env:CODEX_POOLER_API_KEY}",
551+
"baseURL": "http://localhost:4000/v1"
552+
},
553+
"models": {
554+
"gpt-5.5": {
555+
"name": "GPT-5.5 via Codex Pooler",
556+
"tool_call": true,
557+
"reasoning": true,
558+
"temperature": false,
559+
"modalities": {
560+
"input": ["text", "image"],
561+
"output": ["text"]
562+
},
563+
"limit": {
564+
"context": 400000,
565+
"output": 128000
566+
}
567+
}
568+
}
569+
}
570+
}
571+
}
572+
```
573+
574+
Define only model ids your assigned Pool can serve. For deployed instances,
575+
change `baseURL` to `https://codex-pooler.example.com/v1`.
576+
577+
Check the headless path from a repository:
578+
579+
```bash
580+
export CODEX_POOLER_API_KEY=<pool-api-key>
581+
kilo run \
582+
--model openai-compatible/gpt-5.5 \
583+
--pure \
584+
--format json \
585+
--dir "$PWD" \
586+
'Reply with exactly: kilo ok'
587+
```
588+
589+
Use `--auto` only for trusted, isolated automation where Kilo may run approved
590+
tools without prompting. Codex Pooler model use does not require MCP. If you
591+
need operator metadata, use a separate MCP-capable host with an operator MCP
592+
token.
593+
594+
</details>
595+
530596
<details>
531597
<summary><img src=".github/assets/aider-favicon.png" alt="Aider logo" width="16" height="16"> Aider <code>~/.aider.conf.yml</code></summary>
532598

docs-site/public/llms.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Primary public docs:
1717
- https://docs.codex-pooler.com/clients/continue/
1818
- https://docs.codex-pooler.com/clients/cline/
1919
- https://docs.codex-pooler.com/clients/goose/
20+
- https://docs.codex-pooler.com/clients/kilo/
2021
- https://docs.codex-pooler.com/clients/opencode/
2122
- https://docs.codex-pooler.com/clients/openclaw/
2223
- https://docs.codex-pooler.com/clients/openhands/
@@ -78,12 +79,13 @@ Agent guidance:
7879
- Start with https://docs.codex-pooler.com/clients/codex-cli/ when configuring Codex backend clients.
7980
- Start with https://docs.codex-pooler.com/clients/openai-compatible/ when configuring selected OpenAI SDK-compatible clients.
8081
- Use https://docs.codex-pooler.com/clients/openhands/ when configuring OpenHands CLI with LLM_BASE_URL, LLM_MODEL=openai/gpt-5.5, and --override-with-envs.
82+
- Use https://docs.codex-pooler.com/clients/kilo/ when configuring Kilo Code with ~/.config/kilo/kilo.jsonc, model=openai-compatible/gpt-5.5, provider options apiKey={env:CODEX_POOLER_API_KEY}, and baseURL=https://codex-pooler.example.com/v1.
8183
- Use https://docs.codex-pooler.com/clients/pi/ when configuring Pi with ~/.pi/agent/models.json, api=openai-responses, authHeader=true, and a Pool API key in CODEX_POOLER_API_KEY.
8284
- Use https://docs.codex-pooler.com/discovery/self-hosted-codex-gateway/ for short answers about Codex Pooler as a self-hosted Codex gateway.
8385
- Use https://docs.codex-pooler.com/discovery/codex-account-pooling/ for short answers about Pools, upstream accounts, eligibility, and routing.
8486
- Use https://docs.codex-pooler.com/discovery/openai-compatible-codex-gateway/ for short answers about narrow /v1 SDK compatibility over Codex Pools.
8587
- Use https://docs.codex-pooler.com/discovery/codex-pooler-vs-direct-credentials/ for short answers comparing Pool API keys with direct account credentials.
86-
- Use the client-specific pages for Aider, Continue, Cline, Goose, OpenCode, OpenClaw, OpenHands, Pi, Hermes, and Windmill setup snippets.
88+
- Use the client-specific pages for Aider, Continue, Cline, Goose, Kilo, OpenCode, OpenClaw, OpenHands, Pi, Hermes, and Windmill setup snippets.
8789
- Use https://docs.codex-pooler.com/reference/routing-strategies/ when configuring Pools and routing behavior.
8890
- Use https://docs.codex-pooler.com/answers.md for short, citable answers about scope, routes, credentials, privacy, setup, and deployment.
8991
- Keep examples on http://localhost:4000 for local setup or https://codex-pooler.example.com for deployed product examples.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
title: Kilo
3+
description: Configure Kilo Code CLI with Codex Pooler's narrow OpenAI-compatible /v1 surface and a Pool API key.
4+
---
5+
6+
Kilo Code should use Codex Pooler through its OpenAI-compatible provider. Configure `~/.config/kilo/kilo.jsonc`, keep the Pool API key in the environment, and select models with the `openai-compatible/<model>` name that Kilo expects.
7+
8+
## Install
9+
10+
Use the current npm package:
11+
12+
```bash
13+
npm install -g @kilocode/cli
14+
```
15+
16+
For one-off use without a global install, run `npx -y @kilocode/cli@latest`.
17+
18+
## Provider shape
19+
20+
For a deployed instance, add:
21+
22+
```jsonc
23+
{
24+
"$schema": "https://app.kilo.ai/config.json",
25+
"model": "openai-compatible/gpt-5.5",
26+
"enabled_providers": ["openai-compatible"],
27+
"provider": {
28+
"openai-compatible": {
29+
"options": {
30+
"apiKey": "{env:CODEX_POOLER_API_KEY}",
31+
"baseURL": "https://codex-pooler.example.com/v1"
32+
},
33+
"models": {
34+
"gpt-5.5": {
35+
"name": "GPT-5.5 via Codex Pooler",
36+
"tool_call": true,
37+
"reasoning": true,
38+
"temperature": false,
39+
"modalities": {
40+
"input": ["text", "image"],
41+
"output": ["text"]
42+
},
43+
"limit": {
44+
"context": 400000,
45+
"output": 128000
46+
}
47+
}
48+
}
49+
}
50+
}
51+
}
52+
```
53+
54+
For local setup, change `baseURL` to `http://localhost:4000/v1`.
55+
56+
`{env:CODEX_POOLER_API_KEY}` keeps the Pool API key outside the config file. Define only model ids your assigned Pool can serve.
57+
58+
## Smoke check
59+
60+
Run a one-shot prompt from the repository you want Kilo to inspect:
61+
62+
```bash
63+
export CODEX_POOLER_API_KEY=<pool-api-key>
64+
kilo run \
65+
--model openai-compatible/gpt-5.5 \
66+
--pure \
67+
--format json \
68+
--dir "$PWD" \
69+
'Reply with exactly: kilo ok'
70+
```
71+
72+
`--pure` keeps external plugins out of the check. Use `--auto` only in trusted isolated automation, because it lets Kilo approve tool permissions automatically.
73+
74+
## MCP boundary
75+
76+
Kilo model requests use Codex Pooler's narrow OpenAI-compatible `/v1` support for selected SDK routes. Codex Pooler doesn't provide full OpenAI API parity.
77+
78+
Codex Pooler model use does not require MCP. If you need operator metadata from `/mcp`, use a separate MCP-capable host and authenticate it with an operator-owned MCP token, not the Pool API key.

docs-site/src/content/docs/clients/openai-compatible.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Use the dedicated setup pages when configuring an agent or editor that has its o
4242
- [Cline](/clients/cline/)
4343
- [Goose](/clients/goose/)
4444
- [Hermes Agent](/clients/hermes/)
45+
- [Kilo](/clients/kilo/)
4546
- [OpenCode](/clients/opencode/)
4647
- [OpenClaw](/clients/openclaw/)
4748
- [OpenHands](/clients/openhands/)

lib/codex_pooler/gateway/openai_compatibility/chat_completions.ex

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,64 +33,74 @@ defmodule CodexPooler.Gateway.OpenAICompatibility.ChatCompletions do
3333
required(:model) => String.t() | nil,
3434
required(:role_sent?) => boolean(),
3535
required(:include_usage?) => boolean(),
36-
required(:passthrough?) => boolean()
36+
required(:discarding_oversized?) => boolean()
3737
}
3838

39+
@max_incomplete_chat_sse_block_bytes 1_048_576
40+
3941
@spec stream_state(map()) :: stream_state()
4042
def stream_state(chat_payload), do: initial_state(chat_payload)
4143

4244
@spec normalize_stream_data(binary(), stream_state()) :: {binary(), stream_state()}
43-
def normalize_stream_data(data, %{passthrough?: true} = state) when is_binary(data) do
44-
normalize_passthrough_data(data, state)
45+
def normalize_stream_data(data, %{discarding_oversized?: true} = state) when is_binary(data) do
46+
discard_oversized_data(data, state)
4547
end
4648

4749
def normalize_stream_data(data, state) when is_binary(data) and is_map(state) do
4850
buffered_data = state.buffer <> data
49-
{blocks, buffer} = StreamProtocol.complete_sse_blocks(buffered_data, bounded?: true)
51+
{blocks, buffer} = StreamProtocol.complete_sse_blocks(buffered_data, bounded?: false)
5052

51-
if oversized_incomplete_sse_prefix?(blocks, buffer, buffered_data) do
53+
if oversized_incomplete_sse_block?(buffer) do
5254
BufferTelemetry.record_oversized_incomplete(
5355
"public_openai_chat_sse",
5456
byte_size(buffered_data),
55-
StreamProtocol.max_incomplete_sse_block_bytes()
57+
@max_incomplete_chat_sse_block_bytes
5658
)
5759

58-
{buffered_data, %{state | buffer: "", passthrough?: true}}
60+
{iodata, state} = normalize_complete_blocks(blocks, %{state | buffer: ""})
61+
{oversized_iodata, state} = oversized_incomplete_prefix_chunk(buffer, state)
62+
63+
{
64+
[iodata, oversized_iodata] |> IO.iodata_to_binary(),
65+
%{state | buffer: "", discarding_oversized?: true}
66+
}
5967
else
6068
normalize_stream_blocks(blocks, buffer, state)
6169
end
6270
end
6371

6472
def normalize_stream_data(data, state), do: {data, state}
6573

66-
defp normalize_passthrough_data(data, state) do
74+
defp discard_oversized_data(data, state) do
6775
case sse_block_separator(data) do
6876
{index, separator_size} ->
69-
passthrough_size = index + separator_size
70-
passthrough = binary_part(data, 0, passthrough_size)
71-
rest = binary_part(data, passthrough_size, byte_size(data) - passthrough_size)
77+
discard_size = index + separator_size
78+
rest = binary_part(data, discard_size, byte_size(data) - discard_size)
7279

73-
state = %{state | passthrough?: false, buffer: ""}
80+
state = %{state | discarding_oversized?: false, buffer: ""}
7481
{normalized_rest, state} = normalize_stream_data(rest, state)
7582

76-
{[passthrough, normalized_rest] |> IO.iodata_to_binary(), state}
83+
{normalized_rest, state}
7784

7885
nil ->
79-
{data, state}
86+
{"", state}
8087
end
8188
end
8289

8390
defp normalize_stream_blocks(blocks, buffer, state) do
84-
{iodata, state} =
85-
Enum.map_reduce(blocks, %{state | buffer: buffer}, fn block, stream_state ->
86-
normalize_stream_block(block, stream_state)
87-
end)
91+
{iodata, state} = normalize_complete_blocks(blocks, %{state | buffer: buffer})
8892

8993
state = if terminal_blocks?(blocks), do: %{state | buffer: ""}, else: state
9094

9195
{IO.iodata_to_binary(iodata), state}
9296
end
9397

98+
defp normalize_complete_blocks(blocks, state) do
99+
Enum.map_reduce(blocks, state, fn block, stream_state ->
100+
normalize_stream_block(block, stream_state)
101+
end)
102+
end
103+
94104
defp normalize_stream_block("data: [DONE]", state), do: {[], state}
95105

96106
defp normalize_stream_block(block, state) do
@@ -141,10 +151,23 @@ defmodule CodexPooler.Gateway.OpenAICompatibility.ChatCompletions do
141151

142152
defp normalize_stream_event(_type, _decoded, state), do: {[], state}
143153

144-
defp oversized_incomplete_sse_prefix?([], "", data),
145-
do: StreamProtocol.oversized_incomplete_sse_block?(data)
154+
defp oversized_incomplete_sse_block?(buffer),
155+
do: byte_size(buffer) > @max_incomplete_chat_sse_block_bytes
146156

147-
defp oversized_incomplete_sse_prefix?(_blocks, _buffer, _data), do: false
157+
defp oversized_incomplete_prefix_chunk(buffer, state) do
158+
if response_created_prefix?(buffer) do
159+
maybe_role_chunk(state)
160+
else
161+
{[], state}
162+
end
163+
end
164+
165+
defp response_created_prefix?(buffer) do
166+
String.starts_with?(buffer, "event: response.created\n") or
167+
String.starts_with?(buffer, "event: response.created\r\n") or
168+
String.contains?(buffer, "\"type\":\"response.created\"") or
169+
String.contains?(buffer, "\"type\": \"response.created\"")
170+
end
148171

149172
defp maybe_role_chunk(%{role_sent?: true} = state), do: {[], state}
150173

@@ -263,7 +286,7 @@ defmodule CodexPooler.Gateway.OpenAICompatibility.ChatCompletions do
263286
model: Map.get(chat_payload, "model"),
264287
role_sent?: false,
265288
include_usage?: get_in(chat_payload, ["stream_options", "include_usage"]) == true,
266-
passthrough?: false
289+
discarding_oversized?: false
267290
}
268291
end
269292

lib/codex_pooler/gateway/runtime/streaming/downstream_stream.ex

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@ defmodule CodexPooler.Gateway.Runtime.Streaming.DownstreamStream do
6767
buffer == "" and not passthrough?
6868
end
6969

70-
def keepalive_allowed?(%{public_openai_chat: %{buffer: buffer, passthrough?: passthrough?}})
71-
when is_binary(buffer) and is_boolean(passthrough?) do
72-
buffer == "" and not passthrough?
70+
def keepalive_allowed?(%{
71+
public_openai_chat: %{buffer: buffer, discarding_oversized?: discarding_oversized?}
72+
})
73+
when is_binary(buffer) and is_boolean(discarding_oversized?) do
74+
buffer == "" and not discarding_oversized?
7375
end
7476

7577
def keepalive_allowed?(_state), do: true

0 commit comments

Comments
 (0)