feat: add model aliases feature#147
Conversation
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughAdds a storage-backed aliases subsystem, an aliases-aware provider wrapper that rewrites requests, admin API endpoints and UI for alias management, request-model resolution plumbing (context, audit, cache), batch/file rewrite support, storage adapters, and extensive tests. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant AdminAPI as Admin Handler
participant AliasService as Alias Service
participant Store as Alias Store
participant Catalog as Model Catalog
Client->>AdminAPI: PUT /admin/api/v1/aliases/{name}
AdminAPI->>AdminAPI: validate & bind request
AdminAPI->>AliasService: Upsert(alias)
AliasService->>Catalog: LookupModel(target)
Catalog-->>AliasService: model info / support
AliasService->>Store: Upsert(alias)
Store-->>AliasService: OK / error
AliasService->>AliasService: refresh snapshot
AliasService-->>AdminAPI: updated alias / error
AdminAPI-->>Client: 200 / 204 / error
sequenceDiagram
participant Client
participant Handler
participant AliasProvider as Aliases Provider
participant InnerProvider as Concrete Provider
participant AliasService as Alias Service
Client->>Handler: POST /v1/chat (model: "alias-x")
Handler->>AliasProvider: ChatCompletion(req)
AliasProvider->>AliasService: Resolve("alias-x", providerHint)
AliasService-->>AliasProvider: Resolution (concrete model & provider)
AliasProvider->>AliasProvider: rewrite request (substitute model/provider)
AliasProvider->>InnerProvider: ChatCompletion(rewritten req)
InnerProvider-->>AliasProvider: Response
AliasProvider-->>Handler: Response
Handler-->>Client: 200
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/admin/handler.go`:
- Around line 579-583: After Upsert, h.aliases.Get(name) may return false
unexpectedly; instead of silently returning 204, update the handler (the code
calling h.aliases.Get in internal/admin/handler.go) to handle this case
explicitly: either (A) log a warning/error that the alias was not found after
Upsert (include name and any request ID/context) and return a 5xx or 409 to
surface the race/refresh issue, or (B) change the Upsert flow to return the
created/updated alias object from the service and return that payload directly
from the handler (avoid an immediate re-fetch via h.aliases.Get). Locate the Get
call and the surrounding Upsert logic and implement one of these fixes, ensuring
logs include the unique alias name and enough context to debug.
In `@internal/aliases/service.go`:
- Around line 276-278: The background refresh call currently discards any error
from s.Refresh (spawned via refreshCtx, refreshCancel created with
context.WithTimeout), so change the code to capture the returned error from
s.Refresh(refreshCtx), call refreshCancel(), and then log the error (and/or emit
a metric) using the service's logger or metrics helper; ensure you reference
s.Refresh, refreshCtx, refreshCancel and include a clear log message that
includes the error and contextual info (e.g., which alias/service) so failed
refreshes are visible.
In `@internal/providers/registry.go`:
- Around line 472-478: LookupModel currently calls GetModel which returns an
info pointer after releasing the registry lock, then accesses info.Model
(metadata) without synchronization while EnrichModels may mutate it; to fix,
ensure the model data is copied while the registry lock is held: either modify
GetModel to return a deep-cloned core.Model (not the original info pointer)
while holding the lock, or add a new Registry method (e.g., GetModelCopy) that
acquires the same lock and returns a cloned model, and update
ModelRegistry.LookupModel to use that clone so it never reads info.Model after
unlocking; reference symbols: ModelRegistry.LookupModel, GetModel, EnrichModels,
info.Model.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5a70ef66-997b-491e-9352-4184890e3b59
📒 Files selected for processing (16)
internal/admin/handler.gointernal/admin/handler_aliases_test.gointernal/aliases/factory.gointernal/aliases/provider.gointernal/aliases/provider_test.gointernal/aliases/service.gointernal/aliases/service_test.gointernal/aliases/store.gointernal/aliases/store_mongodb.gointernal/aliases/store_postgresql.gointernal/aliases/store_sqlite.gointernal/aliases/types.gointernal/app/app.gointernal/providers/registry.gointernal/server/handlers.gointernal/server/http.go
| alias, ok := h.aliases.Get(name) | ||
| if !ok { | ||
| return c.NoContent(http.StatusNoContent) | ||
| } | ||
| return c.JSON(http.StatusOK, alias) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider clarifying the 204 response after successful upsert.
After a successful Upsert, if Get returns false, returning 204 (No Content) is unusual since the alias was just created. This could indicate a race condition or refresh timing issue.
Consider logging this unexpected scenario or returning the upserted alias data directly from the service instead of re-fetching:
💡 Suggested improvement
- alias, ok := h.aliases.Get(name)
- if !ok {
- return c.NoContent(http.StatusNoContent)
- }
- return c.JSON(http.StatusOK, alias)
+ view := h.aliases.Get(name)
+ if view == nil {
+ // Alias was upserted but not found in snapshot - may indicate refresh timing
+ return c.NoContent(http.StatusAccepted)
+ }
+ return c.JSON(http.StatusOK, view)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/admin/handler.go` around lines 579 - 583, After Upsert,
h.aliases.Get(name) may return false unexpectedly; instead of silently returning
204, update the handler (the code calling h.aliases.Get in
internal/admin/handler.go) to handle this case explicitly: either (A) log a
warning/error that the alias was not found after Upsert (include name and any
request ID/context) and return a 5xx or 409 to surface the race/refresh issue,
or (B) change the Upsert flow to return the created/updated alias object from
the service and return that payload directly from the handler (avoid an
immediate re-fetch via h.aliases.Get). Locate the Get call and the surrounding
Upsert logic and implement one of these fixes, ensuring logs include the unique
alias name and enough context to debug.
| refreshCtx, refreshCancel := context.WithTimeout(ctx, 30*time.Second) | ||
| _ = s.Refresh(refreshCtx) | ||
| refreshCancel() |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Background refresh silently discards errors.
Failed refresh attempts are ignored without any logging or metrics, which could make debugging production issues difficult.
💡 Suggested improvement
case <-ticker.C:
refreshCtx, refreshCancel := context.WithTimeout(ctx, 30*time.Second)
- _ = s.Refresh(refreshCtx)
+ if err := s.Refresh(refreshCtx); err != nil {
+ // Consider adding structured logging here
+ // slog.Warn("alias refresh failed", "error", err)
+ }
refreshCancel()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| refreshCtx, refreshCancel := context.WithTimeout(ctx, 30*time.Second) | |
| _ = s.Refresh(refreshCtx) | |
| refreshCancel() | |
| refreshCtx, refreshCancel := context.WithTimeout(ctx, 30*time.Second) | |
| if err := s.Refresh(refreshCtx); err != nil { | |
| // Consider adding structured logging here | |
| // slog.Warn("alias refresh failed", "error", err) | |
| } | |
| refreshCancel() |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/aliases/service.go` around lines 276 - 278, The background refresh
call currently discards any error from s.Refresh (spawned via refreshCtx,
refreshCancel created with context.WithTimeout), so change the code to capture
the returned error from s.Refresh(refreshCtx), call refreshCancel(), and then
log the error (and/or emit a metric) using the service's logger or metrics
helper; ensure you reference s.Refresh, refreshCtx, refreshCancel and include a
clear log message that includes the error and contextual info (e.g., which
alias/service) so failed refreshes are visible.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/aliases/provider_test.go`:
- Around line 35-38: The StreamChatCompletion mock currently returns
io.NopCloser(nil) which will panic when Read is called; update
providerMock.StreamChatCompletion (and the other mock occurrence) to return a
non-nil, empty reader wrapped with io.NopCloser (for example use
bytes.NewReader([]byte{}) or strings.NewReader("") and wrap it with
io.NopCloser) and add the corresponding import (bytes or strings) so
stream-consuming tests won't panic.
In `@internal/providers/router.go`:
- Around line 392-412: The fast-path using providerTypeLister currently returns
typed.ProviderTypes() raw; update Router.providerTypes to normalize that slice
the same way as the fallback: call typed.ProviderTypes(), iterate the returned
[]string trimming whitespace (strings.TrimSpace), skip empty entries, dedupe
using a seen map[string]struct{}, collect unique values, sort.Strings on the
final slice, and return the sorted, normalized list so both branches produce
deterministic provider type ordering; reference the providerTypes method and the
providerTypeLister.ProviderTypes() call when making the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 510ca9da-59ec-46a3-914a-b06d66e27b06
📒 Files selected for processing (8)
internal/aliases/provider.gointernal/aliases/provider_test.gointernal/core/interfaces.gointernal/guardrails/provider.gointernal/providers/registry.gointernal/providers/router.gointernal/server/handlers.gointernal/server/handlers_test.go
| func (m *providerMock) StreamChatCompletion(_ context.Context, req *core.ChatRequest) (io.ReadCloser, error) { | ||
| m.chatReq = req | ||
| return io.NopCloser(nil), nil | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify no Go code still uses io.NopCloser(nil), which is unsafe for reads.
rg -nP 'io\.NopCloser\s*\(\s*nil\s*\)' --type goRepository: ENTERPILOT/GOModel
Length of output: 474
Avoid io.NopCloser(nil) in stream mocks.
io.NopCloser(nil) can panic on Read; use an empty reader so stream-consuming tests remain safe.
🔧 Proposed fix
import (
"context"
"encoding/json"
"io"
+ "strings"
"testing"
@@
func (m *providerMock) StreamChatCompletion(_ context.Context, req *core.ChatRequest) (io.ReadCloser, error) {
m.chatReq = req
- return io.NopCloser(nil), nil
+ return io.NopCloser(strings.NewReader("")), nil
}
@@
func (m *providerMock) StreamResponses(_ context.Context, req *core.ResponsesRequest) (io.ReadCloser, error) {
m.responsesReq = req
- return io.NopCloser(nil), nil
+ return io.NopCloser(strings.NewReader("")), nil
}Applies to lines 37 and 51.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func (m *providerMock) StreamChatCompletion(_ context.Context, req *core.ChatRequest) (io.ReadCloser, error) { | |
| m.chatReq = req | |
| return io.NopCloser(nil), nil | |
| } | |
| func (m *providerMock) StreamChatCompletion(_ context.Context, req *core.ChatRequest) (io.ReadCloser, error) { | |
| m.chatReq = req | |
| return io.NopCloser(strings.NewReader("")), nil | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/aliases/provider_test.go` around lines 35 - 38, The
StreamChatCompletion mock currently returns io.NopCloser(nil) which will panic
when Read is called; update providerMock.StreamChatCompletion (and the other
mock occurrence) to return a non-nil, empty reader wrapped with io.NopCloser
(for example use bytes.NewReader([]byte{}) or strings.NewReader("") and wrap it
with io.NopCloser) and add the corresponding import (bytes or strings) so
stream-consuming tests won't panic.
| func (r *Router) providerTypes() []string { | ||
| if typed, ok := r.lookup.(providerTypeLister); ok { | ||
| return typed.ProviderTypes() | ||
| } | ||
|
|
||
| seen := make(map[string]struct{}) | ||
| result := make([]string, 0) | ||
| for _, model := range r.lookup.ListModels() { | ||
| providerType := strings.TrimSpace(r.lookup.GetProviderType(model.ID)) | ||
| if providerType == "" { | ||
| continue | ||
| } | ||
| if _, exists := seen[providerType]; exists { | ||
| continue | ||
| } | ||
| seen[providerType] = struct{}{} | ||
| result = append(result, providerType) | ||
| } | ||
| sort.Strings(result) | ||
| return result | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Normalize ProviderTypes() in the fast-path too.
The providerTypeLister branch currently trusts caller ordering/uniqueness, while fallback normalizes. Apply the same trim/dedup/sort pass in both paths to keep inventory deterministic.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/providers/router.go` around lines 392 - 412, The fast-path using
providerTypeLister currently returns typed.ProviderTypes() raw; update
Router.providerTypes to normalize that slice the same way as the fallback: call
typed.ProviderTypes(), iterate the returned []string trimming whitespace
(strings.TrimSpace), skip empty entries, dedupe using a seen
map[string]struct{}, collect unique values, sort.Strings on the final slice, and
return the sorted, normalized list so both branches produce deterministic
provider type ordering; reference the providerTypes method and the
providerTypeLister.ProviderTypes() call when making the change.
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/admin/dashboard/static/css/dashboard.css`:
- Around line 876-880: The .audit-alias-badge rule uses an undefined CSS custom
property --accent-strong, causing the color declaration to be dropped; replace
the token with an existing defined variable (e.g., --accent or
--accent-foreground) or add --accent-strong to both theme palettes so the
variable exists. Update the .audit-alias-badge declaration to reference the
chosen existing token (or add the new --accent-strong variable into each theme
block) so the color resolves consistently across themes.
In `@internal/admin/dashboard/static/js/modules/aliases.js`:
- Around line 23-87: Add a new getter (e.g. scopedDisplayModels or
displayModelsForActiveCategory) that calls the existing displayModels and, when
this.activeCategory is set and not 'all', returns only rows whose category
matches the activeCategory; determine each row's category by preferring
row.provider_type, falling back to row.model?.object or (for alias rows)
alias.provider_type/target model.provider_type as used when building rows in
displayModels; then update table/count/empty-state bindings to use this new
getter instead of displayModels (also make the same change for the equivalent
block referenced at lines 89-105).
In `@internal/admin/dashboard/templates/index.html`:
- Around line 239-242: The filter input (x-model="modelFilter",
class="filter-input") lacks an accessible name; add a stable label by giving the
input an id (e.g., id="model-filter") and adding a corresponding <label
for="model-filter"> with descriptive text like "Filter models" or, if DOM
structure prevents a visible label, add an aria-label="Filter models by
provider, model, alias, or owner" to the same input; ensure the label text
matches the placeholder intent and that nothing else overwrites the id or
aria-label so assistive tech always has a stable name.
In `@internal/aliases/service_test.go`:
- Around line 81-88: The variable named `copy` in the function LookupModel is
shadowing the built-in `copy` identifier and is unclear; rename it (e.g.,
`result` or `modelRef`) and use that new name when returning the pointer (update
the line `copy := value` to `result := value` and return `&result, true`) so
LookupModel no longer shadows the built-in and reads clearly.
- Around line 31-38: The local variable named "copy" in the Get method of
memoryStore shadows Go's builtin copy function; rename it (e.g., result or
aliasRef) and return its address instead (update the declaration "copy := item"
to "result := item" and "return ©, nil" to "return &result, nil") so the
builtin isn't shadowed and intent is clearer.
In `@internal/aliases/service.go`:
- Around line 215-220: The current flow calls s.store.Upsert (and similarly
s.store.Delete) then s.Refresh using the same request context, which can cause a
successful commit to be reported as an error if the context times out; instead,
after a successful s.store.Upsert(ctx, normalized) (or s.store.Delete(ctx,
normalized)), immediately update the in-memory snapshot/state using the
normalized value (or deletion) on the service (e.g., the same snapshot that
Refresh maintains) so the write is reflected even if Refresh later fails, and
keep s.Refresh(ctx) as a best-effort reconciliation step (log/ignore Refresh
errors rather than returning them as the write error).
In `@internal/server/request_model_resolution.go`:
- Around line 25-29: The current code only checks the outer
core.RoutableProvider for resolvedModelProvider, so wrapped providers (e.g.,
internal/guardrails/provider.go) bypass early alias resolution; fix by either
(A) updating wrappers like the guardrails wrapper to implement ResolveModel and
delegate to their wrapped provider, or (B) change the resolution logic in
request_model_resolution.go to unwrap wrappers until a resolvedModelProvider is
found before falling back to core.ParseModelSelector — for example iterate
peeling wrappers (via an Unwrap() method or known wrapper interface) and call
ResolveModel on the first resolver encountered, otherwise call
core.ParseModelSelector as the fallback.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: e0af90ab-4e50-40b2-b7b8-00d80bcb8b7c
📒 Files selected for processing (31)
internal/admin/dashboard/dashboard_test.gointernal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/static/js/dashboard.jsinternal/admin/dashboard/static/js/modules/aliases.jsinternal/admin/dashboard/templates/index.htmlinternal/admin/dashboard/templates/layout.htmlinternal/admin/handler.gointernal/admin/handler_aliases_test.gointernal/aliases/service.gointernal/aliases/service_test.gointernal/aliases/store.gointernal/auditlog/auditlog.gointernal/auditlog/auditlog_test.gointernal/auditlog/middleware.gointernal/auditlog/reader_mongodb.gointernal/auditlog/reader_postgresql.gointernal/auditlog/reader_sqlite.gointernal/auditlog/store_postgresql.gointernal/auditlog/store_sqlite.gointernal/auditlog/store_sqlite_test.gointernal/auditlog/stream_wrapper.gointernal/core/context.gointernal/core/request_model_resolution.gointernal/core/request_model_resolution_test.gointernal/responsecache/middleware_test.gointernal/responsecache/simple.gointernal/server/handlers.gointernal/server/handlers_test.gointernal/server/model_validation.gointernal/server/model_validation_test.gointernal/server/request_model_resolution.go
| .audit-alias-badge { | ||
| background: color-mix(in srgb, var(--accent) 14%, var(--bg)); | ||
| border-color: color-mix(in srgb, var(--accent) 28%, var(--border)); | ||
| color: var(--accent-strong); | ||
| } |
There was a problem hiding this comment.
--accent-strong is undefined here.
Neither theme block defines this token, so this declaration is dropped and the alias badge falls back to the inherited text color. Use an existing token here or add --accent-strong to both theme palettes.
🎨 Suggested fix
.audit-alias-badge {
background: color-mix(in srgb, var(--accent) 14%, var(--bg));
border-color: color-mix(in srgb, var(--accent) 28%, var(--border));
- color: var(--accent-strong);
+ color: var(--accent);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/admin/dashboard/static/css/dashboard.css` around lines 876 - 880,
The .audit-alias-badge rule uses an undefined CSS custom property
--accent-strong, causing the color declaration to be dropped; replace the token
with an existing defined variable (e.g., --accent or --accent-foreground) or add
--accent-strong to both theme palettes so the variable exists. Update the
.audit-alias-badge declaration to reference the chosen existing token (or add
the new --accent-strong variable into each theme block) so the color resolves
consistently across themes.
| get displayModels() { | ||
| const rows = this.models.map((model) => ({ | ||
| key: 'model:' + this.qualifiedModelName(model), | ||
| display_name: this.qualifiedModelName(model), | ||
| secondary_name: '', | ||
| provider_type: model.provider_type || '', | ||
| model: model.model, | ||
| is_alias: false, | ||
| alias: null, | ||
| kind_badge: '', | ||
| masking_alias: null, | ||
| alias_state_class: '', | ||
| alias_state_text: '' | ||
| })); | ||
|
|
||
| if (!this.aliasesAvailable) { | ||
| return rows; | ||
| } | ||
|
|
||
| const maskingAliases = new Map(); | ||
| for (const alias of this.aliases) { | ||
| const aliasName = String(alias && alias.name || '').trim().toLowerCase(); | ||
| if (!aliasName || alias.enabled === false || !alias.valid) { | ||
| continue; | ||
| } | ||
| maskingAliases.set(aliasName, alias); | ||
| } | ||
|
|
||
| for (const row of rows) { | ||
| for (const key of this.modelIdentifierKeys(row.model && row.model.id, row.provider_type)) { | ||
| if (maskingAliases.has(key)) { | ||
| row.masking_alias = maskingAliases.get(key); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for (const alias of this.aliases) { | ||
| const targetModel = this.findConcreteModelForAlias(alias); | ||
| if (!targetModel && this.activeCategory && this.activeCategory !== 'all') { | ||
| continue; | ||
| } | ||
|
|
||
| rows.push({ | ||
| key: 'alias:' + alias.name, | ||
| display_name: alias.name, | ||
| secondary_name: this.aliasTargetLabel(alias), | ||
| provider_type: targetModel ? (targetModel.provider_type || alias.provider_type || '') : (alias.provider_type || ''), | ||
| model: targetModel ? targetModel.model : { id: alias.name, object: 'model' }, | ||
| is_alias: true, | ||
| alias, | ||
| kind_badge: 'Alias', | ||
| masking_alias: null, | ||
| alias_state_class: this.aliasStateClass(alias), | ||
| alias_state_text: this.aliasStateText(alias) | ||
| }); | ||
| } | ||
|
|
||
| return rows.sort((a, b) => { | ||
| if (a.is_alias !== b.is_alias) { | ||
| return a.is_alias ? -1 : 1; | ||
| } | ||
| return String(a.display_name || '').localeCompare(String(b.display_name || '')); | ||
| }); | ||
| }, |
There was a problem hiding this comment.
Category tabs no longer constrain the rows shown.
The only activeCategory check here skips unresolved aliases; concrete rows and resolved aliases always flow into displayModels/filteredDisplayModels. Every category table in internal/admin/dashboard/templates/index.html now renders filteredDisplayModels, so selecting embedding, image, etc. still shows rows from other categories, and the count/empty-state bindings drift from the selected tab as well. Please introduce a category-scoped row getter and use that for the table/count/empty-state bindings.
Also applies to: 89-105
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/admin/dashboard/static/js/modules/aliases.js` around lines 23 - 87,
Add a new getter (e.g. scopedDisplayModels or displayModelsForActiveCategory)
that calls the existing displayModels and, when this.activeCategory is set and
not 'all', returns only rows whose category matches the activeCategory;
determine each row's category by preferring row.provider_type, falling back to
row.model?.object or (for alias rows) alias.provider_type/target
model.provider_type as used when building rows in displayModels; then update
table/count/empty-state bindings to use this new getter instead of displayModels
(also make the same change for the equivalent block referenced at lines 89-105).
| <div class="table-toolbar" x-show="displayModels.length > 0 || modelFilter || aliasesAvailable"> | ||
| <input type="text" placeholder="Filter by provider/model, alias, or owner..." x-model="modelFilter" class="filter-input"> | ||
| <button type="button" class="pagination-btn" x-show="aliasesAvailable" @click="openAliasCreate()">Add Alias</button> | ||
| </div> |
There was a problem hiding this comment.
Give the models filter a real accessible name.
This input only has placeholder text, so assistive tech loses a stable label once the field has content. Add an associated <label> or at least an aria-label.
♿ Suggested fix
- <input type="text" placeholder="Filter by provider/model, alias, or owner..." x-model="modelFilter" class="filter-input">
+ <input type="text" aria-label="Filter models by provider, model, alias, or owner" placeholder="Filter by provider/model, alias, or owner..." x-model="modelFilter" class="filter-input">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div class="table-toolbar" x-show="displayModels.length > 0 || modelFilter || aliasesAvailable"> | |
| <input type="text" placeholder="Filter by provider/model, alias, or owner..." x-model="modelFilter" class="filter-input"> | |
| <button type="button" class="pagination-btn" x-show="aliasesAvailable" @click="openAliasCreate()">Add Alias</button> | |
| </div> | |
| <div class="table-toolbar" x-show="displayModels.length > 0 || modelFilter || aliasesAvailable"> | |
| <input type="text" aria-label="Filter models by provider, model, alias, or owner" placeholder="Filter by provider/model, alias, or owner..." x-model="modelFilter" class="filter-input"> | |
| <button type="button" class="pagination-btn" x-show="aliasesAvailable" `@click`="openAliasCreate()">Add Alias</button> | |
| </div> |
🧰 Tools
🪛 HTMLHint (1.9.2)
[warning] 240-240: No matching [ label ] tag found.
(input-requires-label)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/admin/dashboard/templates/index.html` around lines 239 - 242, The
filter input (x-model="modelFilter", class="filter-input") lacks an accessible
name; add a stable label by giving the input an id (e.g., id="model-filter") and
adding a corresponding <label for="model-filter"> with descriptive text like
"Filter models" or, if DOM structure prevents a visible label, add an
aria-label="Filter models by provider, model, alias, or owner" to the same
input; ensure the label text matches the placeholder intent and that nothing
else overwrites the id or aria-label so assistive tech always has a stable name.
| func (s *memoryStore) Get(_ context.Context, name string) (*Alias, error) { | ||
| item, ok := s.items[name] | ||
| if !ok { | ||
| return nil, ErrNotFound | ||
| } | ||
| copy := item | ||
| return ©, nil | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider renaming copy variable to avoid shadowing builtin.
The variable copy on line 36 shadows Go's builtin copy function. While it works correctly here, renaming it (e.g., result or aliasRef) improves clarity.
♻️ Suggested rename
func (s *memoryStore) Get(_ context.Context, name string) (*Alias, error) {
item, ok := s.items[name]
if !ok {
return nil, ErrNotFound
}
- copy := item
- return ©, nil
+ result := item
+ return &result, nil
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/aliases/service_test.go` around lines 31 - 38, The local variable
named "copy" in the Get method of memoryStore shadows Go's builtin copy
function; rename it (e.g., result or aliasRef) and return its address instead
(update the declaration "copy := item" to "result := item" and "return ©,
nil" to "return &result, nil") so the builtin isn't shadowed and intent is
clearer.
| func (c *testCatalog) LookupModel(model string) (*core.Model, bool) { | ||
| value, ok := c.models[model] | ||
| if !ok { | ||
| return nil, false | ||
| } | ||
| copy := value | ||
| return ©, true | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Same shadowing issue with copy variable.
Similar to line 36, renaming copy to result or modelRef would be clearer.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/aliases/service_test.go` around lines 81 - 88, The variable named
`copy` in the function LookupModel is shadowing the built-in `copy` identifier
and is unclear; rename it (e.g., `result` or `modelRef`) and use that new name
when returning the pointer (update the line `copy := value` to `result := value`
and return `&result, true`) so LookupModel no longer shadows the built-in and
reads clearly.
| if err := s.store.Upsert(ctx, normalized); err != nil { | ||
| return fmt.Errorf("upsert alias: %w", err) | ||
| } | ||
| if err := s.Refresh(ctx); err != nil { | ||
| return fmt.Errorf("refresh aliases: %w", err) | ||
| } |
There was a problem hiding this comment.
A successful write can still return an error here.
store.Upsert()/store.Delete() has already committed by the time Refresh() runs. Because the same request context is reused, a near-deadline request can return 500 after a successful mutation, which invites misleading retries and leaves the in-memory snapshot stale until the next refresh. Consider updating the snapshot from the normalized write/delete result before returning, with Refresh() used as reconciliation instead of part of the write acknowledgment.
Also applies to: 230-235
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/aliases/service.go` around lines 215 - 220, The current flow calls
s.store.Upsert (and similarly s.store.Delete) then s.Refresh using the same
request context, which can cause a successful commit to be reported as an error
if the context times out; instead, after a successful s.store.Upsert(ctx,
normalized) (or s.store.Delete(ctx, normalized)), immediately update the
in-memory snapshot/state using the normalized value (or deletion) on the service
(e.g., the same snapshot that Refresh maintains) so the write is reflected even
if Refresh later fails, and keep s.Refresh(ctx) as a best-effort reconciliation
step (log/ignore Refresh errors rather than returning them as the write error).
| if resolver, ok := provider.(resolvedModelProvider); ok { | ||
| resolvedSelector, aliasApplied, err = resolver.ResolveModel(model, providerHint) | ||
| } else { | ||
| resolvedSelector, err = core.ParseModelSelector(model, providerHint) | ||
| } |
There was a problem hiding this comment.
Wrappers currently bypass early alias resolution.
This only probes the outer core.RoutableProvider for ResolveModel. internal/guardrails/provider.go wraps a provider but does not implement that method, so guarded requests fall back to core.ParseModelSelector() and get adapted/validated against the alias-facing selector instead of the concrete target. Either make wrappers forward ResolveModel or unwrap until a resolver is found before falling back.
As per coding guidelines, "Accept client requests generously (e.g., max_tokens for any model) and adapt them to each provider's specific requirements before forwarding (e.g., translating max_tokens → max_completion_tokens for OpenAI reasoning models)".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/server/request_model_resolution.go` around lines 25 - 29, The
current code only checks the outer core.RoutableProvider for
resolvedModelProvider, so wrapped providers (e.g.,
internal/guardrails/provider.go) bypass early alias resolution; fix by either
(A) updating wrappers like the guardrails wrapper to implement ResolveModel and
delegate to their wrapped provider, or (B) change the resolution logic in
request_model_resolution.go to unwrap wrappers until a resolvedModelProvider is
found before falling back to core.ParseModelSelector — for example iterate
peeling wrappers (via an Unwrap() method or known wrapper interface) and call
ResolveModel on the first resolver encountered, otherwise call
core.ParseModelSelector as the fallback.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/core/batch_preparation.go`:
- Around line 307-317: The current cloneBatchRequest implementation uses json
marshal/unmarshal which can be slow for large BatchRequest; change
cloneBatchRequest to perform a direct struct copy: check for nil req, copy the
top-level struct (cloned := *req), deep-clone the Requests slice by allocating
cloned.Requests = make([]BatchRequestItem, len(req.Requests)) and copy each item
while deep-cloning item.Body via a helper (e.g., CloneRawJSON), and deep-clone
ExtraFields by allocating a new map and copying entries; return &cloned and any
errors from the helper. Ensure you reference cloneBatchRequest, BatchRequest,
Requests, BatchRequestItem, ExtraFields, and the Body field when making these
changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7dd0b042-89ba-4089-a661-fd69867b7581
📒 Files selected for processing (10)
internal/aliases/provider.gointernal/aliases/provider_test.gointernal/batch/store.gointernal/batch/store_test.gointernal/core/batch_preparation.gointernal/core/context.gointernal/guardrails/provider.gointernal/guardrails/provider_test.gointernal/server/handlers.gointernal/server/handlers_test.go
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (3)
internal/admin/dashboard/static/css/dashboard.css (1)
894-897:⚠️ Potential issue | 🟡 Minor
--accent-strongis undefined here.Neither theme block defines that token, so this color declaration is dropped and the alias badge falls back to the inherited text color.
🎨 Suggested fix
.audit-alias-badge { background: color-mix(in srgb, var(--accent) 14%, var(--bg)); border-color: color-mix(in srgb, var(--accent) 28%, var(--border)); - color: var(--accent-strong); + color: var(--accent); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/admin/dashboard/static/css/dashboard.css` around lines 894 - 897, The CSS rule .audit-alias-badge uses an undefined custom property --accent-strong, causing the color to be dropped; fix by either adding a --accent-strong definition in the theme token blocks that define --accent/--bg/--border or replace the reference in .audit-alias-badge with an existing token (e.g., var(--accent) or your theme’s foreground token) so the badge color is defined consistently across themes; update whichever you choose in the theme definitions or in the .audit-alias-badge selector accordingly.internal/admin/dashboard/templates/index.html (1)
239-241:⚠️ Potential issue | 🟡 MinorAdd a stable accessible name to the models filter.
Line 241 still relies on placeholder text, so the input loses its label once it has a value.
♿ Suggested fix
- <input type="text" placeholder="Filter by provider/model, alias, or owner..." x-model="modelFilter" class="filter-input"> + <input type="text" aria-label="Filter models by provider, model, alias, or owner" placeholder="Filter by provider/model, alias, or owner..." x-model="modelFilter" class="filter-input">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/admin/dashboard/templates/index.html` around lines 239 - 241, The models filter input currently only uses placeholder text and loses its accessible name when populated; update the input element that has x-model="modelFilter" and class="filter-input" to have a stable accessible name by either adding a visible <label> tied to the input via id (e.g., id="model-filter" and <label for="model-filter">Models filter</label>) or by adding an aria-label or aria-labelledby attribute (e.g., aria-label="Filter models by provider, alias, or owner") so screen readers always have a persistent label.internal/aliases/provider_test.go (1)
41-43:⚠️ Potential issue | 🟡 MinorUse a non-nil reader in the stream mocks.
io.NopCloser(nil)will panic if any test path reads from the returned stream.🔧 Suggested fix
func (m *providerMock) StreamChatCompletion(_ context.Context, req *core.ChatRequest) (io.ReadCloser, error) { m.chatReq = req - return io.NopCloser(nil), nil + return io.NopCloser(strings.NewReader("")), nil } @@ func (m *providerMock) StreamResponses(_ context.Context, req *core.ResponsesRequest) (io.ReadCloser, error) { m.responsesReq = req - return io.NopCloser(nil), nil + return io.NopCloser(strings.NewReader("")), nil }Run this to confirm there are no remaining
io.NopCloser(nil)mocks after the fix:#!/bin/bash rg -nP 'io\.NopCloser\s*\(\s*nil\s*\)' --type goAlso applies to: 55-57
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/aliases/provider_test.go` around lines 41 - 43, The stream mock providerMock.StreamChatCompletion returns io.NopCloser(nil) which will panic when read; change it to return a non-nil, empty reader (for example wrap an empty byte/string reader with io.NopCloser) and update the other mock(s) at the same file (the similar stub around lines 55-57) so all io.NopCloser(nil) usages are replaced with a safe empty reader; ensure the function signature (providerMock.StreamChatCompletion) still returns (io.ReadCloser, error) and tests compile.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/admin/dashboard/templates/index.html`:
- Around line 314-348: Extract the duplicated model row and action markup into a
single reusable template/component and replace the six repeated blocks with
references to it: create a shared template (e.g., "modelRowTemplate") or Alpine
component that accepts a row and exposes the same bindings used now
(row.display_name, row.is_alias, row.secondary_name, row.masking_alias,
aliasTargetLabel(row.masking_alias), row.model.metadata, formatPrice,
rowAnchorID(row), displayRowClass(row), aliasTogglingName, aliasDeletingName,
and the actions toggleAliasEnabled, openAliasEdit, deleteAlias), then update
each category table to x-for over filteredDisplayModels and instantiate that
shared template/component passing row so behavior and accessibility fixes only
need one edit.
In `@internal/guardrails/provider.go`:
- Around line 554-573: The function mergeBatchHints is duplicated; extract it
into a single shared helper (preserve the signature func mergeBatchHints(left,
right map[string]string) map[string]string) in a common package/module and
replace the duplicate implementations in both provider.go files with calls to
that helper; ensure the helper preserves current semantics (nil when both empty,
copies maps to avoid mutation), update imports/usages accordingly, and remove
the redundant local implementations (keep the helper name mergeBatchHints so
callers remain unchanged).
In `@internal/server/handlers.go`:
- Around line 659-665: The handler currently hard-fails in fileProviderTypes
when h.provider doesn't implement core.NativeFileProviderTypeLister, which
blocks explicit-provider uploads; change the flow so explicit provider
operations are allowed: 1) In the CreateFile request flow, only call
fileProviderTypes when the ?provider= query param is not supplied (i.e., when
routing needs inventory), and 2) make fileProviderTypes tolerant: if h.provider
does not implement core.NativeFileProviderTypeLister return (nil, nil) rather
than an error so callers can distinguish "no inventory available" from a real
error. Update references to fileProviderTypes and CreateFile accordingly.
- Around line 1917-1930: The current refresh logic in the handler overwrites
stored.Batch fields unconditionally (e.g., stored.Batch.Endpoint,
CompletionWindow, RequestCounts, Usage, Results, InProgressAt, CompletedAt,
FailedAt, CancellingAt, CancelledAt), which clears previously known values when
upstream returns partial responses; change each assignment to only copy the
upstream value when it is non-empty/non-zero/non-nil (or use firstNonEmpty
semantics similar to InputFileID) so stored values are preserved
otherwise—either add per-field conditional checks before assigning or introduce
a small helper (e.g., updateIfNonZero) and use it for Endpoint,
CompletionWindow, RequestCounts, Usage, Results and the timestamp fields to
avoid overwriting with empty upstream data.
- Around line 1575-1577: BatchResults is calling
cleanupStoredBatchRewrittenInputFile unconditionally which can delete rewrite
artifacts before the batch reaches a terminal state; modify the logic in the
BatchResults handler so cleanupStoredBatchRewrittenInputFile is only invoked
after confirming the stored batch has a terminal status (same terminal check
used by GetBatch/CancelBatch), e.g. wrap the call to
h.cleanupStoredBatchRewrittenInputFile(ctx, stored) in the terminal-status
condition (or call it after you determine stored.Status is terminal) before
proceeding to h.batchStore.Update(...).
---
Duplicate comments:
In `@internal/admin/dashboard/static/css/dashboard.css`:
- Around line 894-897: The CSS rule .audit-alias-badge uses an undefined custom
property --accent-strong, causing the color to be dropped; fix by either adding
a --accent-strong definition in the theme token blocks that define
--accent/--bg/--border or replace the reference in .audit-alias-badge with an
existing token (e.g., var(--accent) or your theme’s foreground token) so the
badge color is defined consistently across themes; update whichever you choose
in the theme definitions or in the .audit-alias-badge selector accordingly.
In `@internal/admin/dashboard/templates/index.html`:
- Around line 239-241: The models filter input currently only uses placeholder
text and loses its accessible name when populated; update the input element that
has x-model="modelFilter" and class="filter-input" to have a stable accessible
name by either adding a visible <label> tied to the input via id (e.g.,
id="model-filter" and <label for="model-filter">Models filter</label>) or by
adding an aria-label or aria-labelledby attribute (e.g., aria-label="Filter
models by provider, alias, or owner") so screen readers always have a persistent
label.
In `@internal/aliases/provider_test.go`:
- Around line 41-43: The stream mock providerMock.StreamChatCompletion returns
io.NopCloser(nil) which will panic when read; change it to return a non-nil,
empty reader (for example wrap an empty byte/string reader with io.NopCloser)
and update the other mock(s) at the same file (the similar stub around lines
55-57) so all io.NopCloser(nil) usages are replaced with a safe empty reader;
ensure the function signature (providerMock.StreamChatCompletion) still returns
(io.ReadCloser, error) and tests compile.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 6156da6c-9d28-4926-87fd-140a5c72f712
📒 Files selected for processing (13)
internal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/templates/index.htmlinternal/aliases/provider.gointernal/aliases/provider_test.gointernal/batch/store.gointernal/batch/store_test.gointernal/core/batch_preparation.gointernal/core/batch_preparation_test.gointernal/core/context.gointernal/guardrails/provider.gointernal/guardrails/provider_test.gointernal/server/handlers.gointernal/server/handlers_test.go
| <template x-for="row in filteredDisplayModels" :key="row.key"> | ||
| <tr :id="rowAnchorID(row)" :class="displayRowClass(row)"> | ||
| <td> | ||
| <div class="model-name-cell"> | ||
| <div class="model-name-primary"> | ||
| <span class="mono" x-text="row.display_name"></span> | ||
| <span class="alias-kind-badge" x-show="row.is_alias" x-text="row.kind_badge"></span> | ||
| </div> | ||
| <div class="model-name-secondary" x-show="row.is_alias"> | ||
| Targets <span class="mono" x-text="row.secondary_name"></span> | ||
| </div> | ||
| <div class="model-name-secondary" x-show="!row.is_alias && row.masking_alias"> | ||
| Masked by alias to <span class="mono" x-text="aliasTargetLabel(row.masking_alias)"></span> | ||
| </div> | ||
| </div> | ||
| </td> | ||
| <td x-text="(row.model.metadata?.modes ?? []).join(', ') || '-'"></td> | ||
| <td class="col-price" x-text="formatPrice(row.model.metadata?.pricing?.input_per_mtok)"></td> | ||
| <td class="col-price" x-text="formatPrice(row.model.metadata?.pricing?.output_per_mtok)"></td> | ||
| <td class="col-price" x-show="activeCategory === 'text_generation'" x-text="formatPrice(row.model.metadata?.pricing?.cached_input_per_mtok)"></td> | ||
| <td class="model-row-actions"> | ||
| <template x-if="row.is_alias"> | ||
| <div class="alias-actions-cell"> | ||
| <button type="button" class="alias-toggle" :class="{ enabled: row.alias.enabled !== false }" :disabled="aliasTogglingName === row.alias.name" @click="toggleAliasEnabled(row.alias)"> | ||
| <span class="alias-toggle-track"><span class="alias-toggle-thumb"></span></span> | ||
| <span x-text="aliasTogglingName === row.alias.name ? 'Updating...' : (row.alias.enabled !== false ? 'Active' : 'Disabled')"></span> | ||
| </button> | ||
| <button type="button" class="table-action-btn" @click="openAliasEdit(row.alias)">Edit</button> | ||
| <button type="button" class="table-action-btn table-action-btn-danger" :disabled="aliasDeletingName === row.alias.name" @click="deleteAlias(row.alias)" x-text="aliasDeletingName === row.alias.name ? 'Removing...' : 'Remove'"></button> | ||
| </div> | ||
| </template> | ||
| <template x-if="!row.is_alias"> | ||
| <span class="model-row-actions-empty">—</span> | ||
| </template> | ||
| </td> |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Extract the repeated model-row/action markup before it diverges.
The model-name-cell and alias action block is copied into every category table. Any behavior or accessibility fix here now needs six edits, which is easy to miss.
Also applies to: 366-397, 415-446, 465-496, 516-547, 567-598
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/admin/dashboard/templates/index.html` around lines 314 - 348,
Extract the duplicated model row and action markup into a single reusable
template/component and replace the six repeated blocks with references to it:
create a shared template (e.g., "modelRowTemplate") or Alpine component that
accepts a row and exposes the same bindings used now (row.display_name,
row.is_alias, row.secondary_name, row.masking_alias,
aliasTargetLabel(row.masking_alias), row.model.metadata, formatPrice,
rowAnchorID(row), displayRowClass(row), aliasTogglingName, aliasDeletingName,
and the actions toggleAliasEnabled, openAliasEdit, deleteAlias), then update
each category table to x-for over filteredDisplayModels and instantiate that
shared template/component passing row so behavior and accessibility fixes only
need one edit.
| func mergeBatchHints(left, right map[string]string) map[string]string { | ||
| if len(left) == 0 { | ||
| if len(right) == 0 { | ||
| return nil | ||
| } | ||
| merged := make(map[string]string, len(right)) | ||
| for key, value := range right { | ||
| merged[key] = value | ||
| } | ||
| return merged | ||
| } | ||
| merged := make(map[string]string, len(left)) | ||
| for key, value := range left { | ||
| merged[key] = value | ||
| } | ||
| for key, value := range right { | ||
| merged[key] = value | ||
| } | ||
| return merged | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Extract mergeBatchHints into a shared helper.
This function is duplicated with the same logic in internal/aliases/provider.go (Line [408]-[427]). Centralizing it avoids behavior drift across wrappers.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/guardrails/provider.go` around lines 554 - 573, The function
mergeBatchHints is duplicated; extract it into a single shared helper (preserve
the signature func mergeBatchHints(left, right map[string]string)
map[string]string) in a common package/module and replace the duplicate
implementations in both provider.go files with calls to that helper; ensure the
helper preserves current semantics (nil when both empty, copies maps to avoid
mutation), update imports/usages accordingly, and remove the redundant local
implementations (keep the helper name mergeBatchHints so callers remain
unchanged).
| func (h *Handler) fileProviderTypes(ctx *echo.Context) ([]string, error) { | ||
| resp, err := h.provider.ListModels(ctx.Request().Context()) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if resp == nil { | ||
| return []string{}, nil | ||
| } | ||
| seen := make(map[string]struct{}) | ||
| providers := make([]string, 0) | ||
| for _, model := range resp.Data { | ||
| providerType := strings.TrimSpace(h.provider.GetProviderType(model.ID)) | ||
| if providerType == "" { | ||
| continue | ||
| } | ||
| if _, exists := seen[providerType]; exists { | ||
| continue | ||
| } | ||
| seen[providerType] = struct{}{} | ||
| providers = append(providers, providerType) | ||
| typed, ok := h.provider.(core.NativeFileProviderTypeLister) | ||
| if !ok { | ||
| return nil, core.NewProviderError("", http.StatusInternalServerError, "file provider inventory is unavailable", nil) | ||
| } | ||
| sort.Strings(providers) | ||
| return providers, nil | ||
| return typed.NativeFileProviderTypes(), nil | ||
| } |
There was a problem hiding this comment.
Avoid requiring file-inventory support for explicit provider operations.
Line [659] hard-fails when NativeFileProviderTypeLister is missing. Because CreateFile invokes this inventory path before resolving whether ?provider= was supplied, explicit-provider uploads can fail even when native file routing exists.
💡 Proposed fix
func (h *Handler) CreateFile(c *echo.Context) error {
nativeRouter, err := h.nativeFileRouter()
if err != nil {
return handleError(c, err)
}
fileReq, err := fileRouteInfoFromSemantics(c)
if err != nil {
return handleError(c, err)
}
- providers, err := h.fileProviderTypes(c)
- if err != nil {
- return handleError(c, err)
- }
-
providerType := fileReq.Provider
if providerType == "" {
+ providers, err := h.fileProviderTypes(c)
+ if err != nil {
+ return handleError(c, err)
+ }
if len(providers) == 1 {
providerType = providers[0]
} else if len(providers) == 0 {
return handleError(c, core.NewInvalidRequestError("no providers are available for file uploads", nil))
} else {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/server/handlers.go` around lines 659 - 665, The handler currently
hard-fails in fileProviderTypes when h.provider doesn't implement
core.NativeFileProviderTypeLister, which blocks explicit-provider uploads;
change the flow so explicit provider operations are allowed: 1) In the
CreateFile request flow, only call fileProviderTypes when the ?provider= query
param is not supplied (i.e., when routing needs inventory), and 2) make
fileProviderTypes tolerant: if h.provider does not implement
core.NativeFileProviderTypeLister return (nil, nil) rather than an error so
callers can distinguish "no inventory available" from a real error. Update
references to fileProviderTypes and CreateFile accordingly.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
internal/aliases/provider_test.go (1)
41-44:⚠️ Potential issue | 🟡 MinorAvoid nil readers in stream mocks.
Line 43 and Line 57 use
io.NopCloser(nil), which can panic if any read path executes. Use an empty reader instead.🔧 Proposed fix
func (m *providerMock) StreamChatCompletion(_ context.Context, req *core.ChatRequest) (io.ReadCloser, error) { m.chatReq = req - return io.NopCloser(nil), nil + return io.NopCloser(strings.NewReader("")), nil } @@ func (m *providerMock) StreamResponses(_ context.Context, req *core.ResponsesRequest) (io.ReadCloser, error) { m.responsesReq = req - return io.NopCloser(nil), nil + return io.NopCloser(strings.NewReader("")), nil }#!/bin/bash # Verify remaining unsafe nil NopCloser usages in Go code. rg -nP 'io\.NopCloser\s*\(\s*nil\s*\)' --type goAlso applies to: 55-58
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/aliases/provider_test.go` around lines 41 - 44, The StreamChatCompletion implementation in providerMock returns io.NopCloser(nil) which can panic when read is attempted; update providerMock.StreamChatCompletion (and the other test mock at lines ~55-58) to return a safe empty reader instead (for example wrap an empty bytes.Reader or strings.NewReader with io.NopCloser) so the returned io.ReadCloser is non-nil and safe to read/close during tests.internal/server/request_model_resolution.go (1)
26-30:⚠️ Potential issue | 🔴 CriticalResolve aliases through wrapper chains before parse fallback.
Line 26 only probes the outer provider for
ResolveModel. If a wrapper in front of the alias-aware provider does not expose that method, Line 29 falls back tocore.ParseModelSelector, bypassing early alias resolution.#!/bin/bash # 1) Inspect provider wrappers for ResolveModel/Unwrap support. fd 'provider.go' internal | xargs rg -nP 'func\s+\([^)]+\)\s+(ResolveModel|Unwrap)\s*\(' # 2) Inspect request-model-resolution entry points. rg -nP 'resolveRequestModel\(|ensureRequestModelResolution\(|applyRequestModelResolution\(' internal/server # 3) Inspect known wrapper files referenced by this flow. rg -nP 'type\s+\w+\s+struct|func\s+\([^)]+\)\s+ResolveModel\s*\(' internal/guardrails internal/aliases internal/providers🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/server/request_model_resolution.go` around lines 26 - 30, The current logic checks only the outer provider for the resolvedModelProvider interface and falls back to core.ParseModelSelector, which bypasses alias-aware providers wrapped inside other types; update the resolution in resolveRequestModel/ensureRequestModelResolution to iterate unwrap wrappers (calling Unwrap on providers that expose it) until you find a provider that implements resolvedModelProvider, then call its ResolveModel(model, providerHint) and use its resolvedSelector/aliasApplied/err; only if no inner provider implements resolvedModelProvider fall back to core.ParseModelSelector. Ensure you reference the resolvedModelProvider interface, ResolveModel method, Unwrap method on wrapper types, the provider variable, and core.ParseModelSelector when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/admin/dashboard/templates/index.html`:
- Around line 305-319: The current template mounts six tables with x-show,
causing multiple mounted x-for blocks and duplicate row IDs from
rowAnchorID(row); replace those multiple x-show-wrapped tables with a single
table that is conditionally mounted using x-if (or render one shared table) and
pass it a category-scoped list (e.g., derive filteredDisplayModels by
activeCategory) so only the active table is mounted; ensure the template using
x-for="row in filteredDisplayModels" (and helpers rowAnchorID(row) and
displayRowClass(row)) is used once and receives the correct filtered list so IDs
remain unique and rows are not duplicated.
In `@internal/aliases/provider.go`:
- Around line 140-145: The Provider.NativeFileProviderTypes method is making the
alias wrapper always satisfy core.NativeFileProviderTypeLister and thus hides
when the inner provider lacks that capability; remove this method from the
Provider type so the wrapper no longer unconditionally implements
core.NativeFileProviderTypeLister, and instead add a helper on Provider such as
GetNativeFileProviderTypes (or NativeFileProviderTypesOptional) that does an
interface assertion on p.inner for core.NativeFileProviderTypeLister and returns
(types, ok) so callers can detect availability without relying on type
assertions against the wrapper itself; update call sites to use the new helper
if needed.
---
Duplicate comments:
In `@internal/aliases/provider_test.go`:
- Around line 41-44: The StreamChatCompletion implementation in providerMock
returns io.NopCloser(nil) which can panic when read is attempted; update
providerMock.StreamChatCompletion (and the other test mock at lines ~55-58) to
return a safe empty reader instead (for example wrap an empty bytes.Reader or
strings.NewReader with io.NopCloser) so the returned io.ReadCloser is non-nil
and safe to read/close during tests.
In `@internal/server/request_model_resolution.go`:
- Around line 26-30: The current logic checks only the outer provider for the
resolvedModelProvider interface and falls back to core.ParseModelSelector, which
bypasses alias-aware providers wrapped inside other types; update the resolution
in resolveRequestModel/ensureRequestModelResolution to iterate unwrap wrappers
(calling Unwrap on providers that expose it) until you find a provider that
implements resolvedModelProvider, then call its ResolveModel(model,
providerHint) and use its resolvedSelector/aliasApplied/err; only if no inner
provider implements resolvedModelProvider fall back to core.ParseModelSelector.
Ensure you reference the resolvedModelProvider interface, ResolveModel method,
Unwrap method on wrapper types, the provider variable, and
core.ParseModelSelector when making the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: d0caa688-6350-4902-845e-4e03590f96f8
📒 Files selected for processing (6)
internal/admin/dashboard/templates/index.htmlinternal/aliases/provider.gointernal/aliases/provider_test.gointernal/server/handlers_test.gointernal/server/model_validation_test.gointernal/server/request_model_resolution.go
| <div class="table-wrapper" x-show="(displayModels.length > 0 || modelFilter) && (activeCategory === 'all' || activeCategory === 'text_generation')"> | ||
| <table class="data-table"> | ||
| <thead> | ||
| <tr> | ||
| <th>Model ID</th> | ||
| <th>Provider</th> | ||
| <th>Model</th> | ||
| <th>Modes</th> | ||
| <th class="col-price">Input $/MTok</th> | ||
| <th class="col-price">Output $/MTok</th> | ||
| <th class="col-price">Input<br>$/MTok</th> | ||
| <th class="col-price">Output<br>$/MTok</th> | ||
| <th class="col-price" x-show="activeCategory === 'text_generation'">Cached $/MTok</th> | ||
| <th>Actions</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| <template x-for="m in filteredModels" :key="m.model.id"> | ||
| <tr> | ||
| <td class="mono" x-text="m.model.id"></td> | ||
| <td><span class="provider-badge" x-text="m.provider_type"></span></td> | ||
| <td x-text="(m.model.metadata?.modes ?? []).join(', ') || '-'"></td> | ||
| <td class="col-price" x-text="formatPrice(m.model.metadata?.pricing?.input_per_mtok)"></td> | ||
| <td class="col-price" x-text="formatPrice(m.model.metadata?.pricing?.output_per_mtok)"></td> | ||
| <td class="col-price" x-show="activeCategory === 'text_generation'" x-text="formatPrice(m.model.metadata?.pricing?.cached_input_per_mtok)"></td> | ||
| <template x-for="row in filteredDisplayModels" :key="row.key"> | ||
| <tr :id="rowAnchorID(row)" :class="displayRowClass(row)"> |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Alpine.js, does x-showkeep hidden elements in the DOM whilex-if removes them?
💡 Result:
Yes.
x-showtoggles visibility by changing the element’s CSSdisplay(e.g., todisplay: none), so the element stays in the DOM. [1]x-ifadds/removes the element from the DOM entirely (it’s used on a<template>and instantiates/removes the real element). [2]
Sources:
[1] Alpine.js docs — x-show directive https://alpinejs.dev/directives/show
[2] Alpine.js docs — x-if directive / templating https://alpinejs.dev/directives/if , https://alpinejs.dev/essentials/templating
🏁 Script executed:
# Check the actual template file structure to confirm the x-show pattern
cat -n internal/admin/dashboard/templates/index.html | head -600 | tail -300Repository: ENTERPILOT/GOModel
Length of output: 22298
Use x-if (or a single shared table) instead of six x-show tables.
x-show only hides these wrappers, so Alpine still mounts every x-for="row in filteredDisplayModels" block. That gives each model/alias multiple <tr> copies in the DOM, and every copy reuses rowAnchorID(row), which makes row IDs non-unique and any anchor or DOM lookup against them unpredictable. Mount only the active table and feed it a category-scoped row list.
Also applies to: lines 360–371, 409–420, 458–470, 509–521, 560–572
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/admin/dashboard/templates/index.html` around lines 305 - 319, The
current template mounts six tables with x-show, causing multiple mounted x-for
blocks and duplicate row IDs from rowAnchorID(row); replace those multiple
x-show-wrapped tables with a single table that is conditionally mounted using
x-if (or render one shared table) and pass it a category-scoped list (e.g.,
derive filteredDisplayModels by activeCategory) so only the active table is
mounted; ensure the template using x-for="row in filteredDisplayModels" (and
helpers rowAnchorID(row) and displayRowClass(row)) is used once and receives the
correct filtered list so IDs remain unique and rows are not duplicated.
| func (p *Provider) NativeFileProviderTypes() []string { | ||
| if typed, ok := p.inner.(core.NativeFileProviderTypeLister); ok { | ||
| return typed.NativeFileProviderTypes() | ||
| } | ||
| return nil | ||
| } |
There was a problem hiding this comment.
Do not hide missing file-inventory capability behind an empty return.
Line 140 makes the alias wrapper satisfy core.NativeFileProviderTypeLister unconditionally, and Line 144 returns nil when the inner provider lacks that capability. In internal/server/handlers.go (Line 659-665), availability is inferred from type assertion success, so this masks the intended “inventory unavailable” path.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/aliases/provider.go` around lines 140 - 145, The
Provider.NativeFileProviderTypes method is making the alias wrapper always
satisfy core.NativeFileProviderTypeLister and thus hides when the inner provider
lacks that capability; remove this method from the Provider type so the wrapper
no longer unconditionally implements core.NativeFileProviderTypeLister, and
instead add a helper on Provider such as GetNativeFileProviderTypes (or
NativeFileProviderTypesOptional) that does an interface assertion on p.inner for
core.NativeFileProviderTypeLister and returns (types, ok) so callers can detect
availability without relying on type assertions against the wrapper itself;
update call sites to use the new helper if needed.
Summary
/v1/modelsTesting
Summary by CodeRabbit
New Features
Tests