ask-llm-providers 0.10.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3eae68598ee63c7ef7688b717687b06a434f12c740141a3b84bde44752a873d9
4
- data.tar.gz: f21d623f34793e0eff9a08dcb5c1f39c8bba236c541022f1f969a5a273bcb5b7
3
+ metadata.gz: 912f0553986f01615036939eb502aa1f42a35023dc0f04181b970bec4ca3fbbe
4
+ data.tar.gz: 71868d9efd700086b9ad8a3d4ae9f0191239bc4333eed2c889212793abeb1c71
5
5
  SHA512:
6
- metadata.gz: 87589c9effd3da495a57912d6151d44db8c55673cd74d0e50ba6e0c107e90703905e0e94490e5ae880e5e07d27a2375556837cf7da6917bfa9764f7b1028b49d
7
- data.tar.gz: 189cd4cfdfcc19cc1bece8dd6fe561f955dd1e8279a8470cb14549a2d25d05689f06b91687a60920f97237075e5027e287803628579324d98f96a3bd76cffd26
6
+ metadata.gz: 7354ea67a347577694c5cdd8ea3462719a308d8b268f90eb75022618b822f348395ec66cebb2bdb777168fc9f2359fa8d000fb6e647b947f58de84b6cc8dc495
7
+ data.tar.gz: e3c41f929d7118ede14d207e4c6aa480ca8bab95ab8d28a32085e0ba11ffa7ce3b87a24244d54ecaaee14323614840b57af8a312ba13ac62ab843e5fa53431aa
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## [0.11.1] — 2026-08-07
2
+
3
+ ### Added
4
+
5
+ - **GitHub Copilot provider entry + models.** `github_copilot` in the OpenAI-compatible registry (`api.githubcopilot.com` with the `X-GitHub-Api-Version` header) plus catalog models (gpt-5.4, gpt-5.4-nano, gpt-4.1, gpt-4o — flat-rate, riding the user's Copilot subscription via ask-auth's GithubCopilot device OAuth).
6
+
7
+ ## [0.11.0] — 2026-08-07
8
+
9
+ ### Added
10
+
11
+ - **OpenAI Codex provider — ChatGPT subscription access.** `Ask::Providers::OpenaiCodex` always speaks the Responses API at `chatgpt.com/backend-api/codex/responses` with a `ChatGPT-Account-Id` header; supports real **Responses SSE streaming** (`response.output_text.delta` text, `function_call` item/argument events, `response.completed` usage). Pair with ask-auth's `OpenaiCodex` OAuth provider (access token via `api_key:`, account id via `account_id:`). Registered as `:openai_codex`.
12
+ - **Codex models in the catalog** (`openai_codex.json`): gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex-spark — flat-rate (pricing 0) since they ride the user's subscription.
13
+
1
14
  ## [0.10.2] - 2026-08-06
2
15
 
3
16
  ### Fixed
@@ -0,0 +1,86 @@
1
+ [
2
+ {
3
+ "id": "gpt-5.4",
4
+ "name": "GPT-5.4",
5
+ "provider": "github_copilot",
6
+ "family": "gpt",
7
+ "context_window": 400000,
8
+ "max_output_tokens": 128000,
9
+ "capabilities": ["chat", "streaming", "tool_calls"],
10
+ "modalities": { "input": ["text"], "output": ["text"] },
11
+ "pricing": {
12
+ "text_tokens": {
13
+ "standard": {
14
+ "input_per_million": 0.0,
15
+ "output_per_million": 0.0,
16
+ "cache_read_input_per_million": 0.0
17
+ }
18
+ }
19
+ },
20
+ "knowledge_cutoff": "2025-06-01",
21
+ "created_at": "2026-01-01"
22
+ },
23
+ {
24
+ "id": "gpt-5.4-nano",
25
+ "name": "GPT-5.4 nano",
26
+ "provider": "github_copilot",
27
+ "family": "gpt",
28
+ "context_window": 400000,
29
+ "max_output_tokens": 64000,
30
+ "capabilities": ["chat", "streaming", "tool_calls"],
31
+ "modalities": { "input": ["text"], "output": ["text"] },
32
+ "pricing": {
33
+ "text_tokens": {
34
+ "standard": {
35
+ "input_per_million": 0.0,
36
+ "output_per_million": 0.0,
37
+ "cache_read_input_per_million": 0.0
38
+ }
39
+ }
40
+ },
41
+ "knowledge_cutoff": "2025-06-01",
42
+ "created_at": "2026-01-01"
43
+ },
44
+ {
45
+ "id": "gpt-4.1",
46
+ "name": "GPT-4.1",
47
+ "provider": "github_copilot",
48
+ "family": "gpt",
49
+ "context_window": 1000000,
50
+ "max_output_tokens": 32000,
51
+ "capabilities": ["chat", "streaming", "tool_calls"],
52
+ "modalities": { "input": ["text"], "output": ["text"] },
53
+ "pricing": {
54
+ "text_tokens": {
55
+ "standard": {
56
+ "input_per_million": 0.0,
57
+ "output_per_million": 0.0,
58
+ "cache_read_input_per_million": 0.0
59
+ }
60
+ }
61
+ },
62
+ "knowledge_cutoff": "2025-06-01",
63
+ "created_at": "2025-05-01"
64
+ },
65
+ {
66
+ "id": "gpt-4o",
67
+ "name": "GPT-4o",
68
+ "provider": "github_copilot",
69
+ "family": "gpt",
70
+ "context_window": 128000,
71
+ "max_output_tokens": 16000,
72
+ "capabilities": ["chat", "streaming", "tool_calls"],
73
+ "modalities": { "input": ["text"], "output": ["text"] },
74
+ "pricing": {
75
+ "text_tokens": {
76
+ "standard": {
77
+ "input_per_million": 0.0,
78
+ "output_per_million": 0.0,
79
+ "cache_read_input_per_million": 0.0
80
+ }
81
+ }
82
+ },
83
+ "knowledge_cutoff": "2024-10-01",
84
+ "created_at": "2024-05-01"
85
+ }
86
+ ]
@@ -0,0 +1,86 @@
1
+ [
2
+ {
3
+ "id": "gpt-5.5",
4
+ "name": "GPT-5.5",
5
+ "provider": "openai_codex",
6
+ "family": "gpt-codex",
7
+ "context_window": 400000,
8
+ "max_output_tokens": 128000,
9
+ "capabilities": ["chat", "streaming", "tool_calls", "vision"],
10
+ "modalities": { "input": ["text"], "output": ["text"] },
11
+ "pricing": {
12
+ "text_tokens": {
13
+ "standard": {
14
+ "input_per_million": 0.0,
15
+ "output_per_million": 0.0,
16
+ "cache_read_input_per_million": 0.0
17
+ }
18
+ }
19
+ },
20
+ "knowledge_cutoff": "2025-06-01",
21
+ "created_at": "2026-01-01"
22
+ },
23
+ {
24
+ "id": "gpt-5.4",
25
+ "name": "GPT-5.4",
26
+ "provider": "openai_codex",
27
+ "family": "gpt-codex",
28
+ "context_window": 400000,
29
+ "max_output_tokens": 64000,
30
+ "capabilities": ["chat", "streaming", "tool_calls", "vision"],
31
+ "modalities": { "input": ["text"], "output": ["text"] },
32
+ "pricing": {
33
+ "text_tokens": {
34
+ "standard": {
35
+ "input_per_million": 0.0,
36
+ "output_per_million": 0.0,
37
+ "cache_read_input_per_million": 0.0
38
+ }
39
+ }
40
+ },
41
+ "knowledge_cutoff": "2025-06-01",
42
+ "created_at": "2025-11-01"
43
+ },
44
+ {
45
+ "id": "gpt-5.4-mini",
46
+ "name": "GPT-5.4 mini",
47
+ "provider": "openai_codex",
48
+ "family": "gpt-codex",
49
+ "context_window": 400000,
50
+ "max_output_tokens": 64000,
51
+ "capabilities": ["chat", "streaming", "tool_calls", "vision"],
52
+ "modalities": { "input": ["text"], "output": ["text"] },
53
+ "pricing": {
54
+ "text_tokens": {
55
+ "standard": {
56
+ "input_per_million": 0.0,
57
+ "output_per_million": 0.0,
58
+ "cache_read_input_per_million": 0.0
59
+ }
60
+ }
61
+ },
62
+ "knowledge_cutoff": "2025-06-01",
63
+ "created_at": "2025-11-01"
64
+ },
65
+ {
66
+ "id": "gpt-5.3-codex-spark",
67
+ "name": "GPT-5.3 Codex Spark",
68
+ "provider": "openai_codex",
69
+ "family": "gpt-codex",
70
+ "context_window": 400000,
71
+ "max_output_tokens": 64000,
72
+ "capabilities": ["chat", "streaming", "tool_calls", "vision"],
73
+ "modalities": { "input": ["text"], "output": ["text"] },
74
+ "pricing": {
75
+ "text_tokens": {
76
+ "standard": {
77
+ "input_per_million": 0.0,
78
+ "output_per_million": 0.0,
79
+ "cache_read_input_per_million": 0.0
80
+ }
81
+ }
82
+ },
83
+ "knowledge_cutoff": "2025-06-01",
84
+ "created_at": "2025-10-01"
85
+ }
86
+ ]
@@ -75,6 +75,10 @@ module Ask
75
75
  opencode_go: { api_base: "https://opencode.ai/zen/go/v1", api_key_env: "OPENCODE_GO_API_KEY",
76
76
  capabilities: { chat: true, streaming: true, tool_calls: true } },
77
77
 
78
+ github_copilot: { api_base: "https://api.githubcopilot.com", api_key_env: "GITHUB_COPILOT_TOKEN",
79
+ extra_headers: { "X-GitHub-Api-Version" => "2026-06-01" },
80
+ capabilities: { chat: true, streaming: true, tool_calls: true } },
81
+
78
82
  openrouter: { api_base: "https://openrouter.ai/api/v1", api_key_env: "OPENROUTER_API_KEY",
79
83
  extra_headers: { "HTTP-Referer" => "https://github.com/ask-rb",
80
84
  "X-Title" => "ask-rb" },
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module LLM
5
- VERSION = "0.10.2"
5
+ VERSION = "0.12.0"
6
6
  end
7
7
  end
@@ -234,9 +234,27 @@ module Ask
234
234
  # Fall back to text description
235
235
  { type: "text", text: "[#{type} content not supported by Anthropic]" }
236
236
  when "file"
237
- data = block[:data] || block["data"] || ""
238
- filename = block[:filename] ? "[#{block[:filename]}] " : ""
239
- { type: "text", text: "#{filename}#{data}" }
237
+ # Files become Anthropic document blocks: inline data (base64 or
238
+ # text source), a URL, or a provider file reference.
239
+ file_id = block[:file_id] || block["file_id"]
240
+ url = block[:url] || block["url"]
241
+ data = block[:data] || block["data"]
242
+ mime = block[:mime_type] || block["mime_type"]
243
+ filename = block[:filename] || "file"
244
+
245
+ if file_id
246
+ { type: "document", source: { type: "file", file_id: file_id }, title: filename }
247
+ elsif url
248
+ { type: "document", source: { type: "url", url: url }, title: filename }
249
+ elsif data
250
+ if mime.to_s.start_with?("text/")
251
+ { type: "document", source: { type: "text", media_type: (mime || "text/plain"), data: data }, title: filename }
252
+ else
253
+ { type: "document", source: { type: "base64", media_type: (mime || "application/octet-stream"), data: Base64.strict_encode64(data) }, title: filename }
254
+ end
255
+ else
256
+ block
257
+ end
240
258
  else
241
259
  block
242
260
  end
@@ -143,7 +143,13 @@ module Ask
143
143
  google_role = role == "assistant" ? "model" : role
144
144
 
145
145
  parts = []
146
- parts << { text: content } if content
146
+ if content.is_a?(Array)
147
+ # Content blocks → Gemini parts (flat parts array). Previously
148
+ # arrays were passed through as a broken { text: [hash, …] } part.
149
+ content.each { |block| parts.concat(format_google_content_block(block)) }
150
+ elsif content
151
+ parts << { text: content }
152
+ end
147
153
 
148
154
  if msg[:tool_calls] || msg["tool_calls"]
149
155
  (msg[:tool_calls] || msg["tool_calls"]).each do |tc|
@@ -168,6 +174,57 @@ module Ask
168
174
  { role: google_role, parts: }
169
175
  end
170
176
 
177
+ # Gemini content block → flat parts array. Media/file blocks become
178
+ # inlineData (base64) or fileData (uri/file_id); text-like files are
179
+ # inlined as text with a filename marker; unsupported blocks are
180
+ # skipped rather than mangled.
181
+ def format_google_content_block(block)
182
+ block = block.transform_keys(&:to_sym) if block.respond_to?(:transform_keys)
183
+ type = block[:type] || block["type"]
184
+
185
+ case type
186
+ when "text"
187
+ [{ text: block[:text] || block["text"] }]
188
+ when "image", "audio", "video"
189
+ mime = block[:mime_type] || block["mime_type"]
190
+ file_id = block[:file_id] || block["file_id"]
191
+ url = block[:url] || block["url"]
192
+ base64 = block[:base64] || block["base64"]
193
+
194
+ if file_id
195
+ [{ fileData: { mimeType: mime, fileUri: file_id } }]
196
+ elsif url
197
+ [{ fileData: { mimeType: mime, fileUri: url } }]
198
+ elsif base64
199
+ [{ inlineData: { mimeType: (mime || "application/octet-stream"), data: base64 } }]
200
+ else
201
+ []
202
+ end
203
+ when "file"
204
+ mime = block[:mime_type] || block["mime_type"]
205
+ file_id = block[:file_id] || block["file_id"]
206
+ url = block[:url] || block["url"]
207
+ data = block[:data] || block["data"]
208
+
209
+ if file_id
210
+ [{ fileData: { mimeType: mime, fileUri: file_id } }]
211
+ elsif url
212
+ [{ fileData: { mimeType: mime, fileUri: url } }]
213
+ elsif data
214
+ if mime.to_s.start_with?("text/")
215
+ filename = block[:filename] ? "[#{block[:filename]}] " : ""
216
+ [{ text: "#{filename}#{data}" }]
217
+ else
218
+ [{ inlineData: { mimeType: (mime || "application/octet-stream"), data: Base64.strict_encode64(data) } }]
219
+ end
220
+ else
221
+ []
222
+ end
223
+ else
224
+ []
225
+ end
226
+ end
227
+
171
228
  def format_tools(tools)
172
229
  tools.map { |t|
173
230
  {
@@ -279,7 +279,11 @@ module Ask
279
279
  content = msg[:content] || msg["content"] || ""
280
280
 
281
281
  entry = { role: role.to_s }
282
- entry[:content] = [{ type: "input_text", text: content.to_s }]
282
+ entry[:content] = if content.is_a?(Array)
283
+ content.map { |block| format_responses_content_block(block) }
284
+ else
285
+ [{ type: "input_text", text: content.to_s }]
286
+ end
283
287
 
284
288
  # Handle tool calls in assistant messages
285
289
  if (tc = msg[:tool_calls] || msg["tool_calls"]) && tc.respond_to?(:any?) && tc.any?
@@ -302,6 +306,53 @@ module Ask
302
306
  end
303
307
  end
304
308
 
309
+ # Responses API content block → input_* part. File blocks become
310
+ # input_file (inline data URI, URL, or provider file id); images
311
+ # become input_image. Audio/video have no Responses input carrier
312
+ # and degrade to a text note (matching chat completions behavior).
313
+ def format_responses_content_block(block)
314
+ block = block.transform_keys(&:to_sym) if block.respond_to?(:transform_keys)
315
+ type = block[:type] || block["type"]
316
+
317
+ case type
318
+ when "text"
319
+ { type: "input_text", text: block[:text] || block["text"] }
320
+ when "image"
321
+ file_id = block[:file_id] || block["file_id"]
322
+ url = block[:url] || block["url"]
323
+ base64 = block[:base64] || block["base64"]
324
+ if url || base64
325
+ mime = block[:mime_type] || block["mime_type"] || "image/png"
326
+ url ||= Ask::DataURI.from_base64(base64, mime_type: mime)
327
+ { type: "input_image", image_url: url, detail: "auto" }
328
+ else
329
+ { type: "input_image", image_url: file_id, detail: "auto" }
330
+ end
331
+ when "file"
332
+ file_id = block[:file_id] || block["file_id"]
333
+ url = block[:url] || block["url"]
334
+ data = block[:data] || block["data"]
335
+ if file_id
336
+ { type: "input_file", file_id: file_id }
337
+ elsif url
338
+ { type: "input_file", file_url: url }
339
+ elsif data
340
+ mime = block[:mime_type] || block["mime_type"] || "application/octet-stream"
341
+ {
342
+ type: "input_file",
343
+ filename: block[:filename] || "file",
344
+ file_data: Ask::DataURI.from_base64(Base64.strict_encode64(data), mime_type: mime)
345
+ }
346
+ else
347
+ block
348
+ end
349
+ when "audio", "video"
350
+ { type: "input_text", text: "[#{type} content not supported by the Responses API]" }
351
+ else
352
+ block
353
+ end
354
+ end
355
+
305
356
  def extract_responses_provider_results(output, provider_tools)
306
357
  results = {}
307
358
  provider_tool_names = provider_tools.map(&:name)
@@ -393,7 +444,7 @@ module Ask
393
444
  elsif block[:base64] || block["base64"]
394
445
  mime = block[:mime_type] || block["mime_type"] || "image/png"
395
446
  data = block[:base64] || block["base64"]
396
- { type: "image_url", image_url: { url: "data:#{mime};base64,#{data}" } }
447
+ { type: "image_url", image_url: { url: Ask::DataURI.from_base64(data, mime_type: mime) } }
397
448
  elsif block[:file_id] || block["file_id"]
398
449
  { type: "image_url", image_url: { url: block[:file_id] || block["file_id"] } }
399
450
  else
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "openai"
4
+ require_relative "../llm/sse_buffer"
5
+
6
+ module Ask
7
+ module Providers
8
+ # OpenAI Codex — requests routed through a ChatGPT subscription via OAuth
9
+ # (the access token + account id come from ask-auth's OpenaiCodex
10
+ # provider, passed explicitly by the host app: api_key / account_id).
11
+ #
12
+ # Always speaks the Responses API at
13
+ # chatgpt.com/backend-api/codex/responses, and streams the Responses SSE
14
+ # event shape (response.output_text.delta, function_call item events,
15
+ # response.completed) — the chat/completions parsing in the base class
16
+ # doesn't apply here.
17
+ class OpenaiCodex < OpenAI
18
+ API_BASE = "https://chatgpt.com/backend-api/codex"
19
+ SLUG = "openai_codex"
20
+
21
+ def self.slug
22
+ SLUG
23
+ end
24
+
25
+ def api_base
26
+ @config.base_url || API_BASE
27
+ end
28
+
29
+ def headers
30
+ super.tap do |h|
31
+ h["ChatGPT-Account-Id"] = @config.account_id if @config.account_id
32
+ end
33
+ end
34
+
35
+ def chat(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params, &block)
36
+ msgs = messages.is_a?(Ask::Conversation) ? messages.to_a : messages
37
+ regular_tools, = split_tools(tools)
38
+
39
+ payload = {
40
+ model: model,
41
+ input: format_responses_input(msgs)
42
+ }
43
+ payload[:tools] = format_tools(regular_tools) if regular_tools&.any?
44
+ payload[:temperature] = temperature if temperature
45
+ payload.merge!(params)
46
+
47
+ if stream
48
+ codex_stream(payload, model, &block)
49
+ else
50
+ responses_chat_nonstream(payload, model, [])
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def codex_stream(payload, model, &block)
57
+ stream = Ask::Stream.new
58
+ init_sse_buffer
59
+ @http.post("responses") do |req|
60
+ req.body = payload.merge(stream: true)
61
+ req.options.on_data = proc { |data, _bytes, _env| parse_codex_stream(data, stream, model, &block) }
62
+ end.tap do |resp|
63
+ unless resp.success?
64
+ err_body = case resp.body
65
+ when Hash then resp.body
66
+ when String then (JSON.parse(resp.body) rescue { "error" => { "message" => "HTTP #{resp.status}: #{resp.body[0..200]}" } })
67
+ else { "error" => { "message" => "HTTP #{resp.status}: empty response body" } }
68
+ end
69
+ err_body["error"] ||= {}
70
+ err_body["error"]["_status"] = resp.status
71
+ raise LLM::HTTP.map_error(resp.status, err_body, provider: "OpenAI")
72
+ end
73
+ end
74
+ stream.finish!
75
+ stream
76
+ end
77
+
78
+ # Responses-API SSE events:
79
+ # response.output_text.delta -> text content
80
+ # response.output_item.added -> function_call id/name
81
+ # response.function_call_arguments.delta -> tool-call argument fragment
82
+ # response.completed -> usage + finish status
83
+ def parse_codex_stream(raw, stream, model, &block)
84
+ each_sse_event(raw) do |data|
85
+ parsed = JSON.parse(data) rescue next
86
+ chunk =
87
+ case parsed["type"]
88
+ when "response.output_text.delta"
89
+ Ask::Chunk.new(content: parsed["delta"])
90
+ when "response.output_item.added"
91
+ item = parsed["item"] || {}
92
+ next unless item["type"] == "function_call"
93
+
94
+ Ask::Chunk.new(tool_calls: [{index: 0, id: item["id"], name: item["name"]}])
95
+ when "response.function_call_arguments.delta"
96
+ Ask::Chunk.new(tool_calls: [{index: 0, arguments: parsed["arguments"]}])
97
+ when "response.completed"
98
+ response = parsed["response"] || {}
99
+ usage = response["usage"] || {}
100
+ Ask::Chunk.new(
101
+ content: nil,
102
+ finish_reason: response["status"],
103
+ usage: {input_tokens: usage["input_tokens"], output_tokens: usage["output_tokens"]}
104
+ )
105
+ end
106
+ next unless chunk
107
+
108
+ stream.add(chunk)
109
+ yield chunk if block_given?
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -32,6 +32,7 @@ require_relative "ask/provider/bedrock"
32
32
  require_relative "ask/provider/ollama"
33
33
  require_relative "ask/provider/mistral"
34
34
  require_relative "ask/provider/cloudflare"
35
+ require_relative "ask/provider/openai_codex"
35
36
 
36
37
  # Register canonical providers
37
38
  Ask::Provider.register(:openai, Ask::Providers::OpenAI)
@@ -41,6 +42,7 @@ Ask::Provider.register(:bedrock, Ask::Providers::Bedrock)
41
42
  Ask::Provider.register(:ollama, Ask::Providers::Ollama)
42
43
  Ask::Provider.register(:mistral, Ask::Providers::Mistral)
43
44
  Ask::Provider.register(:cloudflare, Ask::Providers::Cloudflare)
45
+ Ask::Provider.register(:openai_codex, Ask::Providers::OpenaiCodex)
44
46
 
45
47
  # Register OpenAI-compatible providers from the registry (data → classes)
46
48
  Ask::LLM::OPENAI_COMPATIBLE.each do |name, cfg|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-llm-providers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.2
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -186,11 +186,13 @@ files:
186
186
  - lib/ask/llm/models/bedrock.json
187
187
  - lib/ask/llm/models/deepseek.json
188
188
  - lib/ask/llm/models/gemini.json
189
+ - lib/ask/llm/models/github_copilot.json
189
190
  - lib/ask/llm/models/meta.json
190
191
  - lib/ask/llm/models/mistral.json
191
192
  - lib/ask/llm/models/moonshot.json
192
193
  - lib/ask/llm/models/nvidia_nim.json
193
194
  - lib/ask/llm/models/openai.json
195
+ - lib/ask/llm/models/openai_codex.json
194
196
  - lib/ask/llm/models/perplexity.json
195
197
  - lib/ask/llm/models/vertex_ai.json
196
198
  - lib/ask/llm/models/xai.json
@@ -208,6 +210,7 @@ files:
208
210
  - lib/ask/provider/mistral.rb
209
211
  - lib/ask/provider/ollama.rb
210
212
  - lib/ask/provider/openai.rb
213
+ - lib/ask/provider/openai_codex.rb
211
214
  - lib/ask/provider/openai_compatible.rb
212
215
  - lib/ask/skills/providers.model_select/SKILL.md
213
216
  homepage: https://github.com/ask-rb/ask-llm-providers