ask-llm-providers 0.13.2 → 0.13.4
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 +4 -4
- data/lib/ask/llm/models.json +22 -0
- data/lib/ask/llm/openai_compatible.rb +5 -0
- data/lib/ask/llm/version.rb +1 -1
- data/lib/ask/provider/openai.rb +38 -6
- data/lib/ask/provider/openai_compatible.rb +16 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1561002d3a643aa61fa750f7074a1a362d45a108f9ea3c44b8d21f0704e6907
|
|
4
|
+
data.tar.gz: d4702a9c31b9a3dda3bea771bd5722a31ccfa5443ebcd4b64a235cafad319d96
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e8c1f0e2f652dbd7031a27278f6e6ead1b23de697450ed14aa0882d2225c0d7392c9de0657504507740199112289ba70adc0ebb33b23427ffb4bc0b3106ef94
|
|
7
|
+
data.tar.gz: 0154ff38051805e13c36fa9c7408ac7fbf5bb38d16362ebe215a9e3e39385b24db822afd2c872cb2a41424993ce4546fc00bf547af927936a2023d6381ab6ef6
|
data/lib/ask/llm/models.json
CHANGED
|
@@ -5076,6 +5076,28 @@
|
|
|
5076
5076
|
"output": 0
|
|
5077
5077
|
}
|
|
5078
5078
|
},
|
|
5079
|
+
{
|
|
5080
|
+
"id": "deepseek/deepseek-v4.1-flash",
|
|
5081
|
+
"name": "DeepSeek V4.1 Flash",
|
|
5082
|
+
"provider": "commandcode",
|
|
5083
|
+
"context_window": 1000000,
|
|
5084
|
+
"capabilities": [
|
|
5085
|
+
"function_calling",
|
|
5086
|
+
"streaming"
|
|
5087
|
+
],
|
|
5088
|
+
"modalities": {
|
|
5089
|
+
"input": [
|
|
5090
|
+
"text"
|
|
5091
|
+
],
|
|
5092
|
+
"output": [
|
|
5093
|
+
"text"
|
|
5094
|
+
]
|
|
5095
|
+
},
|
|
5096
|
+
"pricing": {
|
|
5097
|
+
"input": 0,
|
|
5098
|
+
"output": 0
|
|
5099
|
+
}
|
|
5100
|
+
},
|
|
5079
5101
|
{
|
|
5080
5102
|
"id": "deepseek/deepseek-v4-flash",
|
|
5081
5103
|
"name": "DeepSeek V4 Flash (latest)",
|
|
@@ -76,6 +76,11 @@ module Ask
|
|
|
76
76
|
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
77
77
|
|
|
78
78
|
opencode_go: { api_base: "https://opencode.ai/zen/go/v1", api_key_env: "OPENCODE_GO_API_KEY",
|
|
79
|
+
# The Go gateway routes by session and refuses a request
|
|
80
|
+
# without this header (400 MissingSessionID) — the whole
|
|
81
|
+
# call fails, not just its efficiency. A per-client
|
|
82
|
+
# UUID keeps one agent's requests on one route.
|
|
83
|
+
session_header: "x-opencode-session",
|
|
79
84
|
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
80
85
|
|
|
81
86
|
github_copilot: { api_base: "https://api.githubcopilot.com", api_key_env: "GITHUB_COPILOT_TOKEN",
|
data/lib/ask/llm/version.rb
CHANGED
data/lib/ask/provider/openai.rb
CHANGED
|
@@ -496,16 +496,20 @@ module Ask
|
|
|
496
496
|
def chat_stream(payload, model, &block)
|
|
497
497
|
stream = Ask::Stream.new
|
|
498
498
|
init_sse_buffer
|
|
499
|
+
# The response body is consumed by the stream callback below, so on
|
|
500
|
+
# failure `resp.body` is empty — for exactly the responses whose text
|
|
501
|
+
# matters most. Keeping the first chunk of what actually arrived is
|
|
502
|
+
# what lets an error carry the provider's own words.
|
|
503
|
+
raw = +""
|
|
499
504
|
@http.post("chat/completions") do |req|
|
|
500
505
|
req.body = payload.merge(stream: true)
|
|
501
|
-
req.options.on_data = proc { |data, _bytes, _env|
|
|
506
|
+
req.options.on_data = proc { |data, _bytes, _env|
|
|
507
|
+
raw << data if raw.bytesize < MAX_ERROR_BODY
|
|
508
|
+
parse_stream(data, stream, model, &block)
|
|
509
|
+
}
|
|
502
510
|
end.tap { |resp|
|
|
503
511
|
unless resp.success?
|
|
504
|
-
err_body =
|
|
505
|
-
when Hash then resp.body
|
|
506
|
-
when String then (JSON.parse(resp.body) rescue { "error" => { "message" => "HTTP #{resp.status}: #{resp.body[0..200]}" } })
|
|
507
|
-
else { "error" => { "message" => "HTTP #{resp.status}: empty response body" } }
|
|
508
|
-
end
|
|
512
|
+
err_body = stream_error_body(resp, raw)
|
|
509
513
|
err_body["error"] ||= {}
|
|
510
514
|
err_body["error"]["_status"] = resp.status
|
|
511
515
|
raise LLM::HTTP.map_error(resp.status, err_body, provider: "OpenAI")
|
|
@@ -515,6 +519,34 @@ module Ask
|
|
|
515
519
|
stream
|
|
516
520
|
end
|
|
517
521
|
|
|
522
|
+
# How much of a failed response to keep for the error message. Enough
|
|
523
|
+
# for a JSON error document, not enough to hold a whole streamed body
|
|
524
|
+
# in memory.
|
|
525
|
+
MAX_ERROR_BODY = 4096
|
|
526
|
+
|
|
527
|
+
# The provider's error, from wherever it survived.
|
|
528
|
+
#
|
|
529
|
+
# Faraday hands the body to `on_data` as it arrives, so a response that
|
|
530
|
+
# failed has nothing left in `resp.body` — which is how a specific,
|
|
531
|
+
# actionable 400 ("Request is missing x-opencode-session") reached the
|
|
532
|
+
# operator as "empty response body" and cost an afternoon. Fall back to
|
|
533
|
+
# the buffered text, then to whatever we do have.
|
|
534
|
+
def stream_error_body(resp, raw)
|
|
535
|
+
body = resp.body
|
|
536
|
+
return body if body.is_a?(Hash)
|
|
537
|
+
|
|
538
|
+
text = body.to_s
|
|
539
|
+
text = raw if text.strip.empty?
|
|
540
|
+
parsed = begin
|
|
541
|
+
JSON.parse(text)
|
|
542
|
+
rescue
|
|
543
|
+
nil
|
|
544
|
+
end
|
|
545
|
+
return parsed if parsed.is_a?(Hash) && parsed["error"]
|
|
546
|
+
|
|
547
|
+
{"error" => {"message" => "HTTP #{resp.status}: #{text.to_s.strip[0..400]}"}}
|
|
548
|
+
end
|
|
549
|
+
|
|
518
550
|
def parse_tool_calls(calls)
|
|
519
551
|
return nil unless calls&.any?
|
|
520
552
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
3
5
|
module Ask
|
|
4
6
|
module Providers
|
|
5
7
|
# Single class for all OpenAI-compatible providers.
|
|
@@ -57,9 +59,23 @@ module Ask
|
|
|
57
59
|
if (extra = @compat_cfg[:extra_headers])
|
|
58
60
|
extra.each { |k, v| h[k] = v }
|
|
59
61
|
end
|
|
62
|
+
# A gateway that routes by session needs an id on every request, and
|
|
63
|
+
# a stable one per client so requests land on the same route. This is
|
|
64
|
+
# why the failure mode is a hard 400 rather than a slow call: the
|
|
65
|
+
# gateway cannot place the request at all.
|
|
66
|
+
if (header = @compat_cfg[:session_header])
|
|
67
|
+
h[header] = session_id
|
|
68
|
+
end
|
|
60
69
|
h
|
|
61
70
|
end
|
|
62
71
|
|
|
72
|
+
# One id per provider instance — a conversation's requests stay on one
|
|
73
|
+
# route, which is what the header is for. Memoized so every request
|
|
74
|
+
# from this client carries the same value.
|
|
75
|
+
def session_id
|
|
76
|
+
@session_id ||= SecureRandom.uuid
|
|
77
|
+
end
|
|
78
|
+
|
|
63
79
|
def format_messages(messages)
|
|
64
80
|
result = super
|
|
65
81
|
if @compat_cfg[:reasoning_content]
|