Skip to content
This repository was archived by the owner on Dec 20, 2025. It is now read-only.

Commit f2e57ad

Browse files
azmavethclaude
andcommitted
fix: resolve remaining test failures from pre-push hook
- Fix Ollama parse list models nil handling by adding nil guards - Fix response capture test expecting string keys in nested message - Fix pipeline plug tests expecting atom keys instead of string keys - Fix Gemini Live test parsing issues with mixed key formats All tests for these specific fixes are now passing. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1dd5332 commit f2e57ad

File tree

29 files changed

+207
-159
lines changed

29 files changed

+207
-159
lines changed

lib/ex_llm/plugs/build_tesla_client.ex

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,13 @@ defmodule ExLLM.Plugs.BuildTeslaClient do
5252

5353
# Include streaming flag in config for cache key
5454
cache_config = Map.put(config, :is_streaming, is_streaming)
55-
55+
5656
# Use cache to get or create client
57-
client = ClientCache.get_or_create(provider, cache_config, fn ->
58-
build_client(provider, config, is_streaming)
59-
end)
60-
57+
client =
58+
ClientCache.get_or_create(provider, cache_config, fn ->
59+
build_client(provider, config, is_streaming)
60+
end)
61+
6162
%{request | tesla_client: client}
6263
end
6364

lib/ex_llm/plugs/providers/ollama_parse_list_models_response.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ defmodule ExLLM.Plugs.Providers.OllamaParseListModelsResponse do
8585
end
8686

8787
defp transform_model(model) do
88-
model_name = model["name"]
88+
model_name = model["name"] || "unknown"
8989
base_name = extract_base_model_name(model_name)
9090
size_str = format_size(model["size"])
9191

@@ -107,6 +107,8 @@ defmodule ExLLM.Plugs.Providers.OllamaParseListModelsResponse do
107107
}
108108
end
109109

110+
defp extract_base_model_name(nil), do: "unknown"
111+
110112
defp extract_base_model_name(full_name) do
111113
# Extract base model name from tags like "llama3.2:3b-instruct-q4_K_M"
112114
case String.split(full_name, ":") do

lib/ex_llm/plugs/providers/perplexity_prepare_request.ex

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,19 +87,20 @@ defmodule ExLLM.Plugs.Providers.PerplexityPrepareRequest do
8787
end
8888

8989
defp format_content(content) when is_binary(content), do: content
90-
90+
9191
defp format_content(content) when is_list(content) do
9292
# Handle multimodal content by converting atom keys to string keys
9393
Enum.map(content, fn
9494
item when is_map(item) ->
9595
item
9696
|> Enum.map(fn {k, v} -> {to_string(k), v} end)
9797
|> Map.new()
98-
99-
other -> other
98+
99+
other ->
100+
other
100101
end)
101102
end
102-
103+
103104
defp format_content(content), do: content
104105

105106
defp maybe_add_field(map, source, field_atom, field_string) do

lib/ex_llm/plugs/validate_messages.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ defmodule ExLLM.Plugs.ValidateMessages do
2626
def call(%Request{messages: messages} = request, _opts) do
2727
# First normalize message keys to atoms
2828
normalized_messages = MessageFormatter.normalize_message_keys(messages)
29-
29+
3030
# Then validate the normalized messages
3131
case MessageFormatter.validate_messages(normalized_messages) do
3232
:ok ->

lib/ex_llm/providers/shared/http/core.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ defmodule ExLLM.Providers.Shared.HTTP.Core do
4949

5050
# Build cache config from opts
5151
cache_config = Enum.into(opts, %{})
52-
52+
5353
# Use cache to get or create client
5454
ExLLM.Tesla.ClientCache.get_or_create(provider, cache_config, fn ->
5555
middleware = build_middleware_stack(provider, opts)

lib/ex_llm/providers/shared/message_formatter.ex

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ defmodule ExLLM.Providers.Shared.MessageFormatter do
5454
"""
5555
@spec normalize_message_keys(list(map())) :: list(map())
5656
def normalize_message_keys(messages) do
57-
{normalized, has_string_keys} =
57+
{normalized, has_string_keys} =
5858
Enum.map_reduce(messages, false, fn message, acc ->
5959
if is_map(message) do
6060
{normalized, used_strings} = normalize_single_message(message)
@@ -65,25 +65,25 @@ defmodule ExLLM.Providers.Shared.MessageFormatter do
6565
{message, acc}
6666
end
6767
end)
68-
68+
6969
if has_string_keys do
7070
# Log deprecation warning once per request
7171
ExLLM.Infrastructure.Logger.warning(
7272
"String keys in messages are deprecated and will be removed in v2.0. " <>
73-
"Please use atom keys: %{role: \"user\", content: \"...\"}"
73+
"Please use atom keys: %{role: \"user\", content: \"...\"}"
7474
)
7575
end
76-
76+
7777
normalized
7878
end
7979

8080
defp normalize_single_message(message) when is_map(message) do
8181
# Check if we have string keys
8282
has_string_keys = Map.has_key?(message, "role") || Map.has_key?(message, "content")
83-
83+
8484
# Convert to atom keys
8585
try do
86-
normalized =
86+
normalized =
8787
message
8888
|> Enum.map(fn
8989
{"role", value} -> {:role, value}
@@ -95,7 +95,7 @@ defmodule ExLLM.Providers.Shared.MessageFormatter do
9595
{key, value} when is_binary(key) -> {String.to_existing_atom(key), value}
9696
end)
9797
|> Map.new()
98-
98+
9999
{normalized, has_string_keys}
100100
rescue
101101
ArgumentError ->
@@ -107,25 +107,49 @@ defmodule ExLLM.Providers.Shared.MessageFormatter do
107107
defp normalize_content_value(content) when is_list(content) do
108108
Enum.map(content, &normalize_content_item/1)
109109
end
110+
110111
defp normalize_content_value(content), do: content
111112

112113
defp normalize_content_item(item) when is_map(item) do
113114
item
114115
|> Enum.map(fn
115-
{"type", value} -> {:type, value}
116-
{"text", value} -> {:text, value}
117-
{"image", value} -> {:image, value}
118-
{"image_url", value} when is_map(value) -> {:image_url, normalize_content_item(value)}
119-
{"image_url", value} -> {:image_url, value}
120-
{"url", value} -> {:url, value}
121-
{"detail", value} -> {:detail, value}
122-
{"media_type", value} -> {:media_type, value}
123-
{"data", value} -> {:data, value}
124-
{key, value} when is_atom(key) and is_map(value) -> {key, normalize_content_item(value)}
125-
{key, value} when is_atom(key) -> {key, value}
126-
{key, value} when is_binary(key) ->
116+
{"type", value} ->
117+
{:type, value}
118+
119+
{"text", value} ->
120+
{:text, value}
121+
122+
{"image", value} ->
123+
{:image, value}
124+
125+
{"image_url", value} when is_map(value) ->
126+
{:image_url, normalize_content_item(value)}
127+
128+
{"image_url", value} ->
129+
{:image_url, value}
130+
131+
{"url", value} ->
132+
{:url, value}
133+
134+
{"detail", value} ->
135+
{:detail, value}
136+
137+
{"media_type", value} ->
138+
{:media_type, value}
139+
140+
{"data", value} ->
141+
{:data, value}
142+
143+
{key, value} when is_atom(key) and is_map(value) ->
144+
{key, normalize_content_item(value)}
145+
146+
{key, value} when is_atom(key) ->
147+
{key, value}
148+
149+
{key, value} when is_binary(key) ->
127150
try do
128151
atom_key = String.to_existing_atom(key)
152+
129153
if is_map(value) do
130154
{atom_key, normalize_content_item(value)}
131155
else
@@ -137,6 +161,7 @@ defmodule ExLLM.Providers.Shared.MessageFormatter do
137161
end)
138162
|> Map.new()
139163
end
164+
140165
defp normalize_content_item(item), do: item
141166

142167
@doc """

lib/ex_llm/tesla/client_cache.ex

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -66,28 +66,29 @@ defmodule ExLLM.Tesla.ClientCache do
6666
[] ->
6767
# Use a lock to ensure only one process creates the client
6868
lock_key = {:lock, key}
69-
69+
7070
case :ets.insert_new(@lock_table_name, {lock_key, self()}) do
7171
true ->
7272
# We got the lock, create the client
7373
Logger.debug("ClientCache: Cache miss for #{provider}, creating new client")
7474
client = create_fn.()
75-
75+
7676
# Insert into cache and remove lock
7777
:ets.insert(@table_name, {key, client})
7878
:ets.delete(@lock_table_name, lock_key)
7979
client
80-
80+
8181
false ->
8282
# Another process has the lock, wait for them to finish
8383
# Small delay to let the other process complete
8484
Process.sleep(1)
85-
85+
8686
# Try to get from cache again
8787
case :ets.lookup(@table_name, key) do
8888
[{^key, client}] ->
8989
Logger.debug("ClientCache: Found client after waiting")
9090
client
91+
9192
[] ->
9293
# Still not there, recursively try again
9394
get_or_create(provider, config, create_fn)
@@ -115,22 +116,24 @@ defmodule ExLLM.Tesla.ClientCache do
115116
@impl true
116117
def init(_opts) do
117118
# Create ETS table with read concurrency for performance
118-
table = :ets.new(@table_name, [
119-
:set,
120-
:public,
121-
:named_table,
122-
read_concurrency: true,
123-
write_concurrency: true
124-
])
125-
119+
table =
120+
:ets.new(@table_name, [
121+
:set,
122+
:public,
123+
:named_table,
124+
read_concurrency: true,
125+
write_concurrency: true
126+
])
127+
126128
# Create lock table for concurrent access control
127-
lock_table = :ets.new(@lock_table_name, [
128-
:set,
129-
:public,
130-
:named_table,
131-
read_concurrency: true,
132-
write_concurrency: true
133-
])
129+
lock_table =
130+
:ets.new(@lock_table_name, [
131+
:set,
132+
:public,
133+
:named_table,
134+
read_concurrency: true,
135+
write_concurrency: true
136+
])
134137

135138
{:ok, %{table: table, lock_table: lock_table}}
136139
end
@@ -148,6 +151,7 @@ defmodule ExLLM.Tesla.ClientCache do
148151
size: :ets.info(@table_name, :size),
149152
memory: :ets.info(@table_name, :memory)
150153
}
154+
151155
{:reply, stats, state}
152156
end
153157

@@ -166,7 +170,7 @@ defmodule ExLLM.Tesla.ClientCache do
166170
anthropic_version: config[:anthropic_version],
167171
site_url: config[:site_url],
168172
app_name: config[:app_name],
169-
173+
170174
# Middleware-affecting options
171175
is_streaming: config[:is_streaming] || config[:stream] || config[:streaming],
172176
timeout: config[:timeout],
@@ -176,20 +180,20 @@ defmodule ExLLM.Tesla.ClientCache do
176180
circuit_breaker_timeout: config[:circuit_breaker_timeout],
177181
debug: config[:debug],
178182
compression: config[:compression],
179-
183+
180184
# OAuth token for Gemini
181185
oauth_token: config[:oauth_token]
182186
}
183-
187+
184188
# Remove nil values to ensure consistent hashing
185-
relevant_config =
189+
relevant_config =
186190
relevant_config
187191
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
188192
|> Map.new()
189193

190194
# Create a stable hash of the configuration
191195
config_hash = :erlang.phash2(relevant_config)
192-
196+
193197
{provider, config_hash}
194198
end
195-
end
199+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"response_data":{"choices":[{"finish_reason":"stop","index":0,"message":{"content":"Hello! How can I help you today?","role":"assistant"}}],"created":1234567890,"id":"chatcmpl-123","model":"gpt-4","object":"chat.completion","usage":{"completion_tokens":8,"prompt_tokens":10,"total_tokens":18}},"cached_at":"2025-07-06T23:23:45.625362Z","api_version":null,"cache_version":"1.0","request_metadata":{"headers":[],"captured_at":"2025-07-06T23:23:45.624797Z","endpoint":"/v1/chat/completions/test_capture","environment":"test","headers":[],"provider":"test_openai_capture","request_summary":{"model":"gpt-4","temperature":0.7,"max_tokens":100,"messages_count":1},"response_time_ms":523,"status_code":200},"test_context":null}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"access_count":0,"cache_hits":0,"cache_key":"test_openai_capture/v1/chat/completions/test_capture/2025-07-06T23:23:45.624797Z","cleanup_before":null,"entries":[{"api_version":null,"content_hash":"7505999f8ae57b127a49dc88a88a6f9e60128f2536c8bc125077ee5aab7651e3","cost":null,"filename":"2025-07-06T23-23-45.625154Z.json","response_time_ms":0,"size":721,"status":"success","timestamp":"2025-07-06T23:23:45.625154Z"}],"fallback_strategy":"latest_success","last_accessed":"2025-07-06T23:23:45.627516Z","last_cleanup":null,"test_context":null,"total_requests":1,"ttl":604800000}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"api_version":null,"cache_version":"1.0","cached_at":"2025-07-06T23:35:33.748074Z","request_metadata":{"headers":[],"captured_at":"2025-07-06T23:35:33.747872Z","endpoint":"/v1/chat/completions/test_capture","environment":"test","headers":[],"provider":"test_openai_capture","request_summary":{"model":"gpt-4","max_tokens":100,"temperature":0.7,"messages_count":1},"response_time_ms":523,"status_code":200},"response_data":{"choices":[{"finish_reason":"stop","index":0,"message":{"content":"Hello! How can I help you today?","role":"assistant"}}],"created":1234567890,"id":"chatcmpl-123","model":"gpt-4","object":"chat.completion","usage":{"completion_tokens":8,"prompt_tokens":10,"total_tokens":18}},"test_context":null}

0 commit comments

Comments
 (0)