Skip to content

Conversation

@mostlygeek
Copy link
Owner

@mostlygeek mostlygeek commented Sep 13, 2025

The upstream handler would break on model IDs that contained a forward slash. Model IDs like "aaa/bbb" called at upstream/aaa/bbb would result in an error. This commit adds support for model IDs with a forward slash by iteratively searching the path for a match.

Summary by CodeRabbit

  • New Features
    • Updated /upstream routing to auto-detect the model alias from the path prefix (no separate :model_id segment). The remaining path is forwarded upstream.
    • Supports aliases appearing after multiple leading segments; the first matching prefix is used.
    • Returns 400 if no valid model alias is found; internal errors still return 500.

@coderabbitai
Copy link

coderabbitai bot commented Sep 13, 2025

Walkthrough

Updated the upstream routing from a per-model path parameter to a single wildcard route. The proxy now parses the incoming path to resolve a model alias to a real model name, rewrites the remaining path, swaps to the appropriate process group, and forwards the request using the resolved model.

Changes

Cohort / File(s) Summary
Proxy routing and alias resolution
proxy/proxymanager.go
Changed route from /upstream/:model_id/*upstreamPath to /upstream/*upstreamPath. Implemented path-segment parsing to locate a model alias prefix, map to real model via config.RealModelName, rewrite request path to the remainder, obtain process group with swapProcessGroup, and proxy via processGroup.ProxyRequest(realModelName, ...). Returns 400 if no mapping found, 500 on swap errors.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant PM as ProxyManager
  participant CFG as Config
  participant PG as ProcessGroup
  participant U as Upstream

  C->>PM: HTTP /upstream/{...path}
  PM->>PM: Parse path segments, find alias prefix
  alt alias found
    PM->>CFG: RealModelName(alias)
    CFG-->>PM: realModelName
    PM->>PM: remainingPath = path after alias
    PM->>PM: Rewrite request path to remainingPath
    PM->>PM: swapProcessGroup(alias)
    alt swap ok
      PM->>PG: ProxyRequest(realModelName, writer, request)
      PG->>U: Forward with rewritten path
      U-->>PG: Response
      PG-->>PM: Response
      PM-->>C: Response
    else swap error
      PM-->>C: 500 Internal Server Error
    end
  else no alias found
    PM-->>C: 400 Bad Request
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "upstream handler support for model names with forward slash" accurately and concisely summarizes the PR's primary change: updating the upstream handler to locate and accept model IDs that include forward slashes (so paths like upstream/aaa/bbb are handled). It is specific, directly related to the raw_summary and pr_objectives, and clear enough for a reviewer scanning history to understand the main intent.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch upstream-forward-slash

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mostlygeek mostlygeek added the bug Something isn't working label Sep 13, 2025
@mostlygeek mostlygeek self-assigned this Sep 13, 2025
The upstream handler would break on model IDs that contained a forward
slash. Model IDs like "aaa/bbb" called at upstream/aaa/bbb would result
in an error. This commit adds support for model IDs with a forward slash
by iteratively searching the path for a match.

Updates: #229
@mostlygeek mostlygeek force-pushed the upstream-forward-slash branch from d5b079b to 6a6c6c7 Compare September 13, 2025 20:25
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
proxy/proxymanager.go (4)

230-230: Hook metrics middleware into /upstream route

/upstream traffic should be metered like the other endpoints.

-	pm.ginEngine.Any("/upstream/*upstreamPath", pm.proxyToUpstream)
+	pm.ginEngine.Any("/upstream/*upstreamPath", mm, pm.proxyToUpstream)

396-404: Early‑exit guard; current length check is ineffective

strings.Split("", "/") returns [""] so len(parts) is never 0. Guard on an empty or root path instead.

-	// split the upstream path by / and search for the model name
-	parts := strings.Split(strings.TrimSpace(upstreamPath), "/")
-	if len(parts) == 0 {
-		pm.sendErrorResponse(c, http.StatusBadRequest, "model id required in path")
-		return
-	}
+	// split the upstream path by / and search for the model name
+	upstreamPath = strings.TrimSpace(upstreamPath)
+	if upstreamPath == "" || upstreamPath == "/" {
+		pm.sendErrorResponse(c, http.StatusBadRequest, "model id required in path")
+		return
+	}
+	parts := strings.Split(upstreamPath, "/")

439-441: Handle ProxyRequest error consistently

Other handlers return 500 on ProxyRequest errors; do the same here.

 	// rewrite the path
 	c.Request.URL.Path = remainingPath
-	processGroup.ProxyRequest(realModelName, c.Writer, c.Request)
+	if err := processGroup.ProxyRequest(realModelName, c.Writer, c.Request); err != nil {
+		pm.sendErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("error proxying request: %s", err.Error()))
+		pm.proxyLogger.Errorf("Error Proxying Request for processGroup %s and model %s", processGroup.id, realModelName)
+		return
+	}

395-441: Add tests for slash-containing and overlapping model aliases

Please add upstream tests to cover:

  • Model alias with a slash: /upstream/openai/gpt-4o-mini/v1/chat/completions
  • Overlapping aliases: ensure "aaa/bbb" wins over "aaa" (longest-prefix behavior)

Example test (adjust fixtures as needed):

t.Run("model alias with slash", func(t *testing.T) {
	cfg := fmt.Sprintf(`
logLevel: error
models:
  model1:
    cmd: %s -port ${PORT} -silent -respond model1
    aliases: ["openai/gpt-4o-mini","aaa","aaa/bbb"]
`, getSimpleResponderPath())
	config, err := LoadConfigFromReader(strings.NewReader(cfg))
	assert.NoError(t, err)
	proxy := New(config)
	defer proxy.StopProcesses(StopWaitForInflightRequest)

	// 1) slash alias
	req := httptest.NewRequest("GET", "/upstream/openai/gpt-4o-mini/v1/chat/completions", nil)
	rec := httptest.NewRecorder()
	proxy.ServeHTTP(rec, req)
	assert.Equal(t, http.StatusOK, rec.Code)
	assert.Equal(t, "model1", rec.Body.String())

	// 2) longest-prefix: choose "aaa/bbb" not "aaa"
	req = httptest.NewRequest("GET", "/upstream/aaa/bbb/v1/chat/completions", nil)
	rec = httptest.NewRecorder()
	proxy.ServeHTTP(rec, req)
	assert.Equal(t, http.StatusOK, rec.Code)
	assert.Equal(t, "model1", rec.Body.String())
})
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5b079b and 6a6c6c7.

📒 Files selected for processing (1)
  • proxy/proxymanager.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
proxy/proxymanager.go (3)
proxy/proxymanager_test.go (1)
  • TestProxyManager_Upstream (660-689)
proxy/processgroup.go (1)
  • pg (55-83)
proxy/processgroup_test.go (1)
  • TestProcessGroup_ProxyRequestSwapIsTrueParallel (50-91)

Comment on lines +408 to +425
for i, part := range parts {
if parts[i] == "" {
continue
}

if searchModelName == "" {
searchModelName = part
} else {
searchModelName = searchModelName + "/" + parts[i]
}

if real, ok := pm.config.RealModelName(searchModelName); ok {
modelName = real
remainingPath = "/" + strings.Join(parts[i+1:], "/")
modelFound = true
break
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Prefer longest prefix match for overlapping aliases (e.g., "aaa" vs "aaa/bbb")

Breaking on the first match can misroute requests intended for a longer alias (especially with “%2F” encoded slashes decoded by Gin). Keep scanning and use the last (longest) match.

-	for i, part := range parts {
-		if parts[i] == "" {
+	for i, part := range parts {
+		if part == "" {
 			continue
 		}
 
 		if searchModelName == "" {
-			searchModelName = part
+			searchModelName = part
 		} else {
-			searchModelName = searchModelName + "/" + parts[i]
+			searchModelName += "/" + part
 		}
 
 		if real, ok := pm.config.RealModelName(searchModelName); ok {
 			modelName = real
 			remainingPath = "/" + strings.Join(parts[i+1:], "/")
 			modelFound = true
-			break
+			// keep scanning to prefer the longest matching model alias
 		}
 	}
📝 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.

Suggested change
for i, part := range parts {
if parts[i] == "" {
continue
}
if searchModelName == "" {
searchModelName = part
} else {
searchModelName = searchModelName + "/" + parts[i]
}
if real, ok := pm.config.RealModelName(searchModelName); ok {
modelName = real
remainingPath = "/" + strings.Join(parts[i+1:], "/")
modelFound = true
break
}
}
for i, part := range parts {
if part == "" {
continue
}
if searchModelName == "" {
searchModelName = part
} else {
searchModelName += "/" + part
}
if real, ok := pm.config.RealModelName(searchModelName); ok {
modelName = real
remainingPath = "/" + strings.Join(parts[i+1:], "/")
modelFound = true
// keep scanning to prefer the longest matching model alias
}
}
🤖 Prompt for AI Agents
In proxy/proxymanager.go around lines 408-425, the loop currently breaks on the
first alias match which can pick a shorter alias (e.g., "aaa") over a longer
overlapping alias ("aaa/bbb"); instead, do not break on match — record the
matched real model name and the index where it was found (or compute
remainingPath from that index) and continue scanning all parts so the final
stored match will be the longest prefix; after the loop, use the last recorded
match to set modelName, remainingPath and modelFound (or leave them unchanged if
no match).

@mostlygeek mostlygeek merged commit c36986f into main Sep 13, 2025
3 checks passed
@mostlygeek mostlygeek deleted the upstream-forward-slash branch September 13, 2025 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants