feat: native SAP AI Core support #54

Merged
rodin merged 1 commits from feat/aicore-provider-v2 into main 2026-05-10 20:03:32 +00:00
Owner

Adds native AI Core client for Anthropic models.

Changes

  1. AI Core client (llm/aicore.go)

    • OAuth2 token refresh
    • Deployment discovery
    • Anthropic Messages API via /invoke
    • Uses bedrock-2023-05-31 API version (AI Core uses Bedrock format)
  2. Integration (cmd/review-bot/main.go)

    • New aicore provider option
    • Configured via AICORE_* env vars
  3. CI (.gitea/workflows/ci.yml)

    • Switched from HAI proxy to native AI Core
    • Updated model names

Testing

  • Unit tests pass
  • Verified via integration testing against live AI Core endpoint

Closes #49

Adds native AI Core client for Anthropic models. ## Changes 1. **AI Core client** (`llm/aicore.go`) - OAuth2 token refresh - Deployment discovery - Anthropic Messages API via `/invoke` - Uses `bedrock-2023-05-31` API version (AI Core uses Bedrock format) 2. **Integration** (`cmd/review-bot/main.go`) - New `aicore` provider option - Configured via `AICORE_*` env vars 3. **CI** (`.gitea/workflows/ci.yml`) - Switched from HAI proxy to native AI Core - Updated model names ## Testing - Unit tests pass - Verified via integration testing against live AI Core endpoint Closes #49
rodin self-assigned this 2026-05-10 08:03:29 +00:00
sonnet-review-bot approved these changes 2026-05-10 08:04:19 +00:00
sonnet-review-bot left a comment
First-time contributor

Original review

Supersededsee current review for up-to-date findings.

Previous findings (commit 4931481a)

Sonnet Review

Summary

This PR adds a well-structured SAP AI Core provider to the LLM client, with proper OAuth2 token caching, deployment discovery, and routing between Anthropic and OpenAI model types. The implementation is correct, idiomatic Go, and has comprehensive unit tests covering the core behaviors. A few minor issues exist but none are blocking.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 57 The deployment struct has an ID field that is populated from the deployments API response but never used anywhere in the code. If it's genuinely unused, it adds noise; if it was intended for logging or future use, a comment explaining its purpose would help. Consider removing it or adding a comment.
2 [MINOR] llm/aicore.go 137 The deployment cache is never invalidated. If an AI Core deployment becomes unavailable (transitions from RUNNING to STOPPED) after the initial fetch, the cached stale URL will be used until the process restarts. This is likely acceptable for the current use case (short-lived CI jobs), but could cause hard-to-debug failures in longer-lived deployments. A TTL on the deployment cache would make this more robust.
3 [MINOR] llm/aicore.go 119 In getDeploymentURL, there's a potential inefficiency: getToken is called before acquiring the write lock, which is correct for avoiding deadlock, but if fetchDeployments fails and the function is called again concurrently, multiple goroutines could redundantly call getToken (which itself acquires a lock). This is a minor performance nit rather than a correctness issue since getToken is idempotent and safe to call concurrently.
4 [NIT] llm/aicore.go 33 The field name http shadows the net/http package import. While Go resolves this correctly (the field is accessed via the receiver, the package via the bare name), it's a minor readability concern. The existing llm/client.go uses the same pattern so it's consistent within the codebase, but something like httpClient would be clearer.
5 [NIT] llm/aicore_test.go 164 The comment // Use a pointer to capture the server URL for use in the handler is slightly misleading — baseURL is a string, not a pointer. The actual mechanism is that the closure captures the variable by reference. The comment could be dropped or rephrased.
6 [NIT] .gitea/actions/review/action.yml 62 Trailing whitespace on the default: 'default' line for aicore-resource-group. Minor but inconsistent with the rest of the file.

Recommendation

APPROVE — Approve. The implementation is solid: correct double-checked locking for token caching, proper RLock/RUnlock upgrade pattern, clean separation between the AICoreClient and the top-level Client, comprehensive tests using httptest, and good error wrapping throughout. The CI passes. The minor issues noted (unused field, stale deployment cache, shadowed import name) are not blocking for the intended use case of short-lived CI jobs. The code is idiomatic Go and follows the project's established patterns.


Review by sonnet


Evaluated against 4931481a

~~Original review~~ **Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/54#pullrequestreview-2009) for up-to-date findings. <details><summary>Previous findings (commit 4931481a)</summary> # Sonnet Review ## Summary This PR adds a well-structured SAP AI Core provider to the LLM client, with proper OAuth2 token caching, deployment discovery, and routing between Anthropic and OpenAI model types. The implementation is correct, idiomatic Go, and has comprehensive unit tests covering the core behaviors. A few minor issues exist but none are blocking. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 57 | The `deployment` struct has an `ID` field that is populated from the deployments API response but never used anywhere in the code. If it's genuinely unused, it adds noise; if it was intended for logging or future use, a comment explaining its purpose would help. Consider removing it or adding a comment. | | 2 | [MINOR] | `llm/aicore.go` | 137 | The deployment cache is never invalidated. If an AI Core deployment becomes unavailable (transitions from RUNNING to STOPPED) after the initial fetch, the cached stale URL will be used until the process restarts. This is likely acceptable for the current use case (short-lived CI jobs), but could cause hard-to-debug failures in longer-lived deployments. A TTL on the deployment cache would make this more robust. | | 3 | [MINOR] | `llm/aicore.go` | 119 | In `getDeploymentURL`, there's a potential inefficiency: `getToken` is called before acquiring the write lock, which is correct for avoiding deadlock, but if `fetchDeployments` fails and the function is called again concurrently, multiple goroutines could redundantly call `getToken` (which itself acquires a lock). This is a minor performance nit rather than a correctness issue since `getToken` is idempotent and safe to call concurrently. | | 4 | [NIT] | `llm/aicore.go` | 33 | The field name `http` shadows the `net/http` package import. While Go resolves this correctly (the field is accessed via the receiver, the package via the bare name), it's a minor readability concern. The existing `llm/client.go` uses the same pattern so it's consistent within the codebase, but something like `httpClient` would be clearer. | | 5 | [NIT] | `llm/aicore_test.go` | 164 | The comment `// Use a pointer to capture the server URL for use in the handler` is slightly misleading — `baseURL` is a `string`, not a pointer. The actual mechanism is that the closure captures the variable by reference. The comment could be dropped or rephrased. | | 6 | [NIT] | `.gitea/actions/review/action.yml` | 62 | Trailing whitespace on the `default: 'default' ` line for `aicore-resource-group`. Minor but inconsistent with the rest of the file. | ## Recommendation **APPROVE** — Approve. The implementation is solid: correct double-checked locking for token caching, proper RLock/RUnlock upgrade pattern, clean separation between the `AICoreClient` and the top-level `Client`, comprehensive tests using `httptest`, and good error wrapping throughout. The CI passes. The minor issues noted (unused field, stale deployment cache, shadowed import name) are not blocking for the intended use case of short-lived CI jobs. The code is idiomatic Go and follows the project's established patterns. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 4931481a* </details> <!-- review-bot:sonnet -->
@@ -41,0 +59,4 @@
aicore-resource-group:
description: 'SAP AI Core resource group (default: default)'
required: false
default: 'default'
First-time contributor

[NIT] Trailing whitespace on the default: 'default' line for aicore-resource-group. Minor but inconsistent with the rest of the file.

**[NIT]** Trailing whitespace on the `default: 'default' ` line for `aicore-resource-group`. Minor but inconsistent with the rest of the file.
llm/aicore.go Outdated
@@ -0,0 +30,4 @@
// Thread-safe for concurrent use after construction.
type AICoreClient struct {
config AICoreConfig
http *http.Client
First-time contributor

[NIT] The field name http shadows the net/http package import. While Go resolves this correctly (the field is accessed via the receiver, the package via the bare name), it's a minor readability concern. The existing llm/client.go uses the same pattern so it's consistent within the codebase, but something like httpClient would be clearer.

**[NIT]** The field name `http` shadows the `net/http` package import. While Go resolves this correctly (the field is accessed via the receiver, the package via the bare name), it's a minor readability concern. The existing `llm/client.go` uses the same pattern so it's consistent within the codebase, but something like `httpClient` would be clearer.
llm/aicore.go Outdated
@@ -0,0 +54,4 @@
}
// WithTimeout sets the HTTP request timeout for AI Core calls.
// This should be called during construction, before concurrent use.
First-time contributor

[MINOR] The deployment struct has an ID field that is populated from the deployments API response but never used anywhere in the code. If it's genuinely unused, it adds noise; if it was intended for logging or future use, a comment explaining its purpose would help. Consider removing it or adding a comment.

**[MINOR]** The `deployment` struct has an `ID` field that is populated from the deployments API response but never used anywhere in the code. If it's genuinely unused, it adds noise; if it was intended for logging or future use, a comment explaining its purpose would help. Consider removing it or adding a comment.
llm/aicore.go Outdated
@@ -0,0 +116,4 @@
return "", time.Time{}, fmt.Errorf("token request failed (status %d): %s", resp.StatusCode, string(body))
}
var tokenResp struct {
First-time contributor

[MINOR] In getDeploymentURL, there's a potential inefficiency: getToken is called before acquiring the write lock, which is correct for avoiding deadlock, but if fetchDeployments fails and the function is called again concurrently, multiple goroutines could redundantly call getToken (which itself acquires a lock). This is a minor performance nit rather than a correctness issue since getToken is idempotent and safe to call concurrently.

**[MINOR]** In `getDeploymentURL`, there's a potential inefficiency: `getToken` is called before acquiring the write lock, which is correct for avoiding deadlock, but if `fetchDeployments` fails and the function is called again concurrently, multiple goroutines could redundantly call `getToken` (which itself acquires a lock). This is a minor performance nit rather than a correctness issue since `getToken` is idempotent and safe to call concurrently.
llm/aicore.go Outdated
@@ -0,0 +134,4 @@
// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.
func (c *AICoreClient) getDeploymentURL(ctx context.Context, model string) (string, error) {
c.mu.RLock()
First-time contributor

[MINOR] The deployment cache is never invalidated. If an AI Core deployment becomes unavailable (transitions from RUNNING to STOPPED) after the initial fetch, the cached stale URL will be used until the process restarts. This is likely acceptable for the current use case (short-lived CI jobs), but could cause hard-to-debug failures in longer-lived deployments. A TTL on the deployment cache would make this more robust.

**[MINOR]** The deployment cache is never invalidated. If an AI Core deployment becomes unavailable (transitions from RUNNING to STOPPED) after the initial fetch, the cached stale URL will be used until the process restarts. This is likely acceptable for the current use case (short-lived CI jobs), but could cause hard-to-debug failures in longer-lived deployments. A TTL on the deployment cache would make this more robust.
@@ -0,0 +161,4 @@
// Should error on unknown model
_, err = client.getDeploymentURL(context.Background(), "unknown-model")
if err == nil {
First-time contributor

[NIT] The comment // Use a pointer to capture the server URL for use in the handler is slightly misleading — baseURL is a string, not a pointer. The actual mechanism is that the closure captures the variable by reference. The comment could be dropped or rephrased.

**[NIT]** The comment `// Use a pointer to capture the server URL for use in the handler` is slightly misleading — `baseURL` is a `string`, not a pointer. The actual mechanism is that the closure captures the variable by reference. The comment could be dropped or rephrased.
rodin force-pushed feat/aicore-provider-v2 from 4931481a63 to 5b3f6b1a44 2026-05-10 08:05:05 +00:00 Compare
security-review-bot requested review from security-review-bot 2026-05-10 08:05:26 +00:00
security-review-bot approved these changes 2026-05-10 08:05:26 +00:00
Dismissed
security-review-bot left a comment
Collaborator

Security Review

Summary

The changes add a native SAP AI Core provider with OAuth2, deployment discovery, and appropriate integration points. Overall, the implementation handles authentication, avoids logging secrets, and respects timeouts; CI passed. I found a few minor hardening opportunities around enforcing HTTPS and limiting error body leakage.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 85 Auth and API endpoints are accepted as-is without enforcing HTTPS. Using plain HTTP would expose client credentials and tokens in transit. Consider validating scheme and warning or rejecting non-HTTPS URLs for AICoreConfig.AuthURL and APIURL.
2 [MINOR] llm/aicore.go 118 On token request failure, the code includes the entire response body in the error string ("token request failed ...: %s"). This can lead to information leakage in logs if the auth server returns diagnostic details. Consider truncating or redacting bodies in error messages.
3 [MINOR] llm/aicore.go 204 On deployments API failure, the error message embeds the full response body ("deployments request failed...: %s"). This may leak internal details to logs. Consider truncating the body to a small prefix or omitting it.
4 [MINOR] llm/aicore.go 323 On Anthropic invoke API failure, the code returns the full response body in the error string. For defense-in-depth, avoid logging entire bodies from upstream services; truncate or sanitize to prevent accidental leakage of sensitive information.
5 [MINOR] llm/aicore.go 385 On OpenAI chat completions failure, the error includes the full response body. Similar to other endpoints, consider truncating/sanitizing the body to reduce information exposure in logs.

Recommendation

APPROVE — Overall the implementation is sound and adds native SAP AI Core support without introducing obvious security vulnerabilities. The OAuth flow, token caching, and deployment discovery are implemented correctly and are not logged. For defense-in-depth, consider enforcing or at least warning on non-HTTPS AICore URLs and avoid including full upstream response bodies in error messages to limit information leakage in logs. With these minor improvements in mind, this change is safe to merge.


Review by security


Evaluated against 4931481a

# Security Review ## Summary The changes add a native SAP AI Core provider with OAuth2, deployment discovery, and appropriate integration points. Overall, the implementation handles authentication, avoids logging secrets, and respects timeouts; CI passed. I found a few minor hardening opportunities around enforcing HTTPS and limiting error body leakage. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 85 | Auth and API endpoints are accepted as-is without enforcing HTTPS. Using plain HTTP would expose client credentials and tokens in transit. Consider validating scheme and warning or rejecting non-HTTPS URLs for AICoreConfig.AuthURL and APIURL. | | 2 | [MINOR] | `llm/aicore.go` | 118 | On token request failure, the code includes the entire response body in the error string ("token request failed ...: %s"). This can lead to information leakage in logs if the auth server returns diagnostic details. Consider truncating or redacting bodies in error messages. | | 3 | [MINOR] | `llm/aicore.go` | 204 | On deployments API failure, the error message embeds the full response body ("deployments request failed...: %s"). This may leak internal details to logs. Consider truncating the body to a small prefix or omitting it. | | 4 | [MINOR] | `llm/aicore.go` | 323 | On Anthropic invoke API failure, the code returns the full response body in the error string. For defense-in-depth, avoid logging entire bodies from upstream services; truncate or sanitize to prevent accidental leakage of sensitive information. | | 5 | [MINOR] | `llm/aicore.go` | 385 | On OpenAI chat completions failure, the error includes the full response body. Similar to other endpoints, consider truncating/sanitizing the body to reduce information exposure in logs. | ## Recommendation **APPROVE** — Overall the implementation is sound and adds native SAP AI Core support without introducing obvious security vulnerabilities. The OAuth flow, token caching, and deployment discovery are implemented correctly and are not logged. For defense-in-depth, consider enforcing or at least warning on non-HTTPS AICore URLs and avoid including full upstream response bodies in error messages to limit information leakage in logs. With these minor improvements in mind, this change is safe to merge. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against 4931481a*
llm/aicore.go Outdated
@@ -0,0 +82,4 @@
if err != nil {
return "", err
}
c.token = token
Collaborator

[MINOR] Auth and API endpoints are accepted as-is without enforcing HTTPS. Using plain HTTP would expose client credentials and tokens in transit. Consider validating scheme and warning or rejecting non-HTTPS URLs for AICoreConfig.AuthURL and APIURL.

**[MINOR]** Auth and API endpoints are accepted as-is without enforcing HTTPS. Using plain HTTP would expose client credentials and tokens in transit. Consider validating scheme and warning or rejecting non-HTTPS URLs for AICoreConfig.AuthURL and APIURL.
llm/aicore.go Outdated
@@ -0,0 +115,4 @@
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", time.Time{}, fmt.Errorf("token request failed (status %d): %s", resp.StatusCode, string(body))
}
Collaborator

[MINOR] On token request failure, the code includes the entire response body in the error string ("token request failed ...: %s"). This can lead to information leakage in logs if the auth server returns diagnostic details. Consider truncating or redacting bodies in error messages.

**[MINOR]** On token request failure, the code includes the entire response body in the error string ("token request failed ...: %s"). This can lead to information leakage in logs if the auth server returns diagnostic details. Consider truncating or redacting bodies in error messages.
llm/aicore.go Outdated
@@ -0,0 +201,4 @@
Name string `json:"name"`
} `json:"model"`
} `json:"backend_details"`
} `json:"resources"`
Collaborator

[MINOR] On deployments API failure, the error message embeds the full response body ("deployments request failed...: %s"). This may leak internal details to logs. Consider truncating the body to a small prefix or omitting it.

**[MINOR]** On deployments API failure, the error message embeds the full response body ("deployments request failed...: %s"). This may leak internal details to logs. Consider truncating the body to a small prefix or omitting it.
llm/aicore.go Outdated
@@ -0,0 +320,4 @@
deployURL, err := c.getDeploymentURL(ctx, model)
if err != nil {
return "", err
}
Collaborator

[MINOR] On Anthropic invoke API failure, the code returns the full response body in the error string. For defense-in-depth, avoid logging entire bodies from upstream services; truncate or sanitize to prevent accidental leakage of sensitive information.

**[MINOR]** On Anthropic invoke API failure, the code returns the full response body in the error string. For defense-in-depth, avoid logging entire bodies from upstream services; truncate or sanitize to prevent accidental leakage of sensitive information.
gpt-review-bot approved these changes 2026-05-10 08:05:31 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Original review

Supersededsee current review for up-to-date findings.

Previous findings (commit 5b3f6b1a)

Gpt Review

Summary

Solid, well-scoped addition of native SAP AI Core support with clear provider gating, thorough input validation, and comprehensive tests. CI updates and README/action docs are consistent with the new provider.

Recommendation

APPROVE — The new AI Core client is thoughtfully implemented: OAuth2 token caching with RWMutex, deployment discovery, and separate Anthropic/OpenAI invocation paths match expected SAP patterns. main.go validates provider-specific inputs correctly and cleanly integrates the AICoreConfig. The composite action and CI workflow changes align with the new provider and are documented in README with clear examples. Tests cover token refresh, deployment selection, both invocation paths, WithAICore configuration, and timeout propagation using httptest. Consider a future refactor to reuse a common HTTP response handling helper (similar to doRequest) for AI Core paths to include Content-Length mismatch checks for parity with the OpenAI/Anthropic code path and improve retry classification. Otherwise, this is ready to merge.


Review by gpt


Evaluated against 4931481a

~~Original review~~ **Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/54#pullrequestreview-2010) for up-to-date findings. <details><summary>Previous findings (commit 5b3f6b1a)</summary> # Gpt Review ## Summary Solid, well-scoped addition of native SAP AI Core support with clear provider gating, thorough input validation, and comprehensive tests. CI updates and README/action docs are consistent with the new provider. ## Recommendation **APPROVE** — The new AI Core client is thoughtfully implemented: OAuth2 token caching with RWMutex, deployment discovery, and separate Anthropic/OpenAI invocation paths match expected SAP patterns. main.go validates provider-specific inputs correctly and cleanly integrates the AICoreConfig. The composite action and CI workflow changes align with the new provider and are documented in README with clear examples. Tests cover token refresh, deployment selection, both invocation paths, WithAICore configuration, and timeout propagation using httptest. Consider a future refactor to reuse a common HTTP response handling helper (similar to doRequest) for AI Core paths to include Content-Length mismatch checks for parity with the OpenAI/Anthropic code path and improve retry classification. Otherwise, this is ready to merge. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 4931481a* </details> <!-- review-bot:gpt -->
sonnet-review-bot approved these changes 2026-05-10 08:05:58 +00:00
sonnet-review-bot left a comment
First-time contributor

Original review

Supersededsee current review for up-to-date findings.

Previous findings (commit 5b3f6b1a)

Sonnet Review

Summary

This PR adds a well-structured SAP AI Core provider with proper OAuth2 token caching, deployment discovery, and routing between Anthropic and OpenAI backends. The implementation follows established patterns in the codebase, has comprehensive tests, and CI passes.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 162 Deployment cache is never invalidated. Once deployments are fetched, they are cached indefinitely. If a deployment goes down and is replaced (new deployment URL, same model name), the client will continue using the stale URL until restart. Consider adding a TTL or a re-fetch-on-error path (e.g., clear the cache and retry when CompleteAnthropic/CompleteOpenAI gets a non-2xx response).
2 [MINOR] llm/aicore.go 190 When multiple models exist with the same name and both are RUNNING, the last one encountered wins (the loop overwrites c.deployments[modelName]). There is no tie-breaking and the outcome is non-deterministic depending on API response order. The test for this scenario (deploy-456 is STOPPED, deploy-789 is RUNNING for gpt-5) works correctly, but if two RUNNING deployments for the same model name exist the behaviour is undefined. A comment explaining the intent would help.
3 [MINOR] llm/aicore.go 68 The deployment.ID field is stored but never used — only deployment.URL is referenced. This is dead storage. Either use it (e.g. in log messages for diagnostics) or remove the field and the struct altogether in favour of storing just the URL string.
4 [NIT] llm/aicore.go 35 The struct field http *http.Client shadows the stdlib package name http. While it compiles fine, it can confuse readers and linters. Consider renaming to httpClient *http.Client (consistent with how other Go stdlib code names embedded HTTP clients).
5 [NIT] llm/aicore.go 224 The anthropicRequest struct's AnthropicVersion field is set to the hard-coded string "bedrock-2023-05-31" at the call site rather than using a named constant. A package-level constant like AICoreAnthropicVersion would make this easier to update and self-documenting, similar to the already-defined AICoreOpenAIAPIVersion.
6 [NIT] .gitea/actions/review/action.yml 65 Trailing space on the default: 'default' line (aicore-resource-group default value). Minor formatting inconsistency — matches the pre-existing similar trailing space on the original llm-provider default that was fixed in this PR.
7 [NIT] llm/aicore_test.go 6 The time package is imported but used only in TestAICoreClient_TokenExpiry and TestAICoreClient_WithTimeout. Not a problem, just noting that removing "time" from the import if those tests were moved would be needed. Current state is correct.

Recommendation

APPROVE — Approve with minor observations. The core implementation is correct and idiomatic: the double-checked locking pattern for token/deployment caching is sound, error wrapping follows fmt.Errorf("%w") convention throughout, tests use httptest and table-driven patterns appropriately, and the provider routing logic is clean. The most actionable finding is the missing deployment cache invalidation (MINOR) — if a deployment URL changes at runtime the client will be stuck until restart, which may be acceptable for this use case but worth documenting explicitly. The other findings are low-impact observations.


Review by sonnet


Evaluated against 5b3f6b1a

~~Original review~~ **Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/54#pullrequestreview-2014) for up-to-date findings. <details><summary>Previous findings (commit 5b3f6b1a)</summary> # Sonnet Review ## Summary This PR adds a well-structured SAP AI Core provider with proper OAuth2 token caching, deployment discovery, and routing between Anthropic and OpenAI backends. The implementation follows established patterns in the codebase, has comprehensive tests, and CI passes. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 162 | Deployment cache is never invalidated. Once deployments are fetched, they are cached indefinitely. If a deployment goes down and is replaced (new deployment URL, same model name), the client will continue using the stale URL until restart. Consider adding a TTL or a re-fetch-on-error path (e.g., clear the cache and retry when CompleteAnthropic/CompleteOpenAI gets a non-2xx response). | | 2 | [MINOR] | `llm/aicore.go` | 190 | When multiple models exist with the same name and both are RUNNING, the last one encountered wins (the loop overwrites `c.deployments[modelName]`). There is no tie-breaking and the outcome is non-deterministic depending on API response order. The test for this scenario (`deploy-456` is STOPPED, `deploy-789` is RUNNING for gpt-5) works correctly, but if two RUNNING deployments for the same model name exist the behaviour is undefined. A comment explaining the intent would help. | | 3 | [MINOR] | `llm/aicore.go` | 68 | The `deployment.ID` field is stored but never used — only `deployment.URL` is referenced. This is dead storage. Either use it (e.g. in log messages for diagnostics) or remove the field and the struct altogether in favour of storing just the URL string. | | 4 | [NIT] | `llm/aicore.go` | 35 | The struct field `http *http.Client` shadows the stdlib package name `http`. While it compiles fine, it can confuse readers and linters. Consider renaming to `httpClient *http.Client` (consistent with how other Go stdlib code names embedded HTTP clients). | | 5 | [NIT] | `llm/aicore.go` | 224 | The `anthropicRequest` struct's `AnthropicVersion` field is set to the hard-coded string `"bedrock-2023-05-31"` at the call site rather than using a named constant. A package-level constant like `AICoreAnthropicVersion` would make this easier to update and self-documenting, similar to the already-defined `AICoreOpenAIAPIVersion`. | | 6 | [NIT] | `.gitea/actions/review/action.yml` | 65 | Trailing space on the `default: 'default' ` line (aicore-resource-group default value). Minor formatting inconsistency — matches the pre-existing similar trailing space on the original `llm-provider` default that was fixed in this PR. | | 7 | [NIT] | `llm/aicore_test.go` | 6 | The `time` package is imported but used only in `TestAICoreClient_TokenExpiry` and `TestAICoreClient_WithTimeout`. Not a problem, just noting that removing `"time"` from the import if those tests were moved would be needed. Current state is correct. | ## Recommendation **APPROVE** — Approve with minor observations. The core implementation is correct and idiomatic: the double-checked locking pattern for token/deployment caching is sound, error wrapping follows `fmt.Errorf("%w")` convention throughout, tests use `httptest` and table-driven patterns appropriately, and the provider routing logic is clean. The most actionable finding is the missing deployment cache invalidation (MINOR) — if a deployment URL changes at runtime the client will be stuck until restart, which may be acceptable for this use case but worth documenting explicitly. The other findings are low-impact observations. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 5b3f6b1a* </details> <!-- review-bot:sonnet -->
@@ -41,3 +62,4 @@
default: 'default'
conventions-file:
description: 'Path to conventions file in the repo (e.g. CLAUDE.md)'
required: false
First-time contributor

[NIT] Trailing space on the default: 'default' line (aicore-resource-group default value). Minor formatting inconsistency — matches the pre-existing similar trailing space on the original llm-provider default that was fixed in this PR.

**[NIT]** Trailing space on the `default: 'default' ` line (aicore-resource-group default value). Minor formatting inconsistency — matches the pre-existing similar trailing space on the original `llm-provider` default that was fixed in this PR.
llm/aicore.go Outdated
@@ -0,0 +32,4 @@
config AICoreConfig
http *http.Client
mu sync.RWMutex
First-time contributor

[NIT] The struct field http *http.Client shadows the stdlib package name http. While it compiles fine, it can confuse readers and linters. Consider renaming to httpClient *http.Client (consistent with how other Go stdlib code names embedded HTTP clients).

**[NIT]** The struct field `http *http.Client` shadows the stdlib package name `http`. While it compiles fine, it can confuse readers and linters. Consider renaming to `httpClient *http.Client` (consistent with how other Go stdlib code names embedded HTTP clients).
llm/aicore.go Outdated
@@ -0,0 +65,4 @@
c.mu.RLock()
if c.token != "" && time.Now().Add(5*time.Minute).Before(c.tokenExpiry) {
token := c.token
c.mu.RUnlock()
First-time contributor

[MINOR] The deployment.ID field is stored but never used — only deployment.URL is referenced. This is dead storage. Either use it (e.g. in log messages for diagnostics) or remove the field and the struct altogether in favour of storing just the URL string.

**[MINOR]** The `deployment.ID` field is stored but never used — only `deployment.URL` is referenced. This is dead storage. Either use it (e.g. in log messages for diagnostics) or remove the field and the struct altogether in favour of storing just the URL string.
llm/aicore.go Outdated
@@ -0,0 +159,4 @@
return "", err
}
if d, ok := c.deployments[model]; ok {
First-time contributor

[MINOR] Deployment cache is never invalidated. Once deployments are fetched, they are cached indefinitely. If a deployment goes down and is replaced (new deployment URL, same model name), the client will continue using the stale URL until restart. Consider adding a TTL or a re-fetch-on-error path (e.g., clear the cache and retry when CompleteAnthropic/CompleteOpenAI gets a non-2xx response).

**[MINOR]** Deployment cache is never invalidated. Once deployments are fetched, they are cached indefinitely. If a deployment goes down and is replaced (new deployment URL, same model name), the client will continue using the stale URL until restart. Consider adding a TTL or a re-fetch-on-error path (e.g., clear the cache and retry when CompleteAnthropic/CompleteOpenAI gets a non-2xx response).
llm/aicore.go Outdated
@@ -0,0 +187,4 @@
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("deployments request failed (status %d): %s", resp.StatusCode, string(body))
}
First-time contributor

[MINOR] When multiple models exist with the same name and both are RUNNING, the last one encountered wins (the loop overwrites c.deployments[modelName]). There is no tie-breaking and the outcome is non-deterministic depending on API response order. The test for this scenario (deploy-456 is STOPPED, deploy-789 is RUNNING for gpt-5) works correctly, but if two RUNNING deployments for the same model name exist the behaviour is undefined. A comment explaining the intent would help.

**[MINOR]** When multiple models exist with the same name and both are RUNNING, the last one encountered wins (the loop overwrites `c.deployments[modelName]`). There is no tie-breaking and the outcome is non-deterministic depending on API response order. The test for this scenario (`deploy-456` is STOPPED, `deploy-789` is RUNNING for gpt-5) works correctly, but if two RUNNING deployments for the same model name exist the behaviour is undefined. A comment explaining the intent would help.
llm/aicore.go Outdated
@@ -0,0 +221,4 @@
ID: r.ID,
URL: r.DeploymentURL,
}
}
First-time contributor

[NIT] The anthropicRequest struct's AnthropicVersion field is set to the hard-coded string "bedrock-2023-05-31" at the call site rather than using a named constant. A package-level constant like AICoreAnthropicVersion would make this easier to update and self-documenting, similar to the already-defined AICoreOpenAIAPIVersion.

**[NIT]** The `anthropicRequest` struct's `AnthropicVersion` field is set to the hard-coded string `"bedrock-2023-05-31"` at the call site rather than using a named constant. A package-level constant like `AICoreAnthropicVersion` would make this easier to update and self-documenting, similar to the already-defined `AICoreOpenAIAPIVersion`.
@@ -0,0 +3,4 @@
import (
"context"
"encoding/json"
"fmt"
First-time contributor

[NIT] The time package is imported but used only in TestAICoreClient_TokenExpiry and TestAICoreClient_WithTimeout. Not a problem, just noting that removing "time" from the import if those tests were moved would be needed. Current state is correct.

**[NIT]** The `time` package is imported but used only in `TestAICoreClient_TokenExpiry` and `TestAICoreClient_WithTimeout`. Not a problem, just noting that removing `"time"` from the import if those tests were moved would be needed. Current state is correct.
gpt-review-bot approved these changes 2026-05-10 08:06:41 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Original review

Supersededsee current review for up-to-date findings.

Previous findings (commit 5b3f6b1a)

Gpt Review

Summary

The PR cleanly adds native SAP AI Core support with a well-structured AICore client, integrates it into the CLI and composite action, and updates CI and docs accordingly. Concurrency, error handling, and configuration patterns are sound and idiomatic, and tests comprehensively cover the new functionality.

Recommendation

APPROVE — Overall implementation is solid and idiomatic: token caching and deployment discovery are thread-safe, error handling is contextual, and the CLI correctly validates inputs per provider. The composite action and CI workflow changes align with the new provider and avoid unnecessary requirements for AI Core. Documentation updates are clear. Approve as-is.


Review by gpt


Evaluated against 5b3f6b1a

~~Original review~~ **Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/54#pullrequestreview-2016) for up-to-date findings. <details><summary>Previous findings (commit 5b3f6b1a)</summary> # Gpt Review ## Summary The PR cleanly adds native SAP AI Core support with a well-structured AICore client, integrates it into the CLI and composite action, and updates CI and docs accordingly. Concurrency, error handling, and configuration patterns are sound and idiomatic, and tests comprehensively cover the new functionality. ## Recommendation **APPROVE** — Overall implementation is solid and idiomatic: token caching and deployment discovery are thread-safe, error handling is contextual, and the CLI correctly validates inputs per provider. The composite action and CI workflow changes align with the new provider and avoid unnecessary requirements for AI Core. Documentation updates are clear. Approve as-is. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 5b3f6b1a* </details> <!-- review-bot:gpt -->
security-review-bot requested review from security-review-bot 2026-05-10 08:06:58 +00:00
security-review-bot requested changes 2026-05-10 08:06:58 +00:00
Dismissed
security-review-bot left a comment
Collaborator

Security Review

Summary

CI is failing for the PR, which requires changes before merge. The new SAP AI Core client and integration appear generally sound with no critical vulnerabilities, but there are minor hardening issues around lock usage during network I/O and potential information leakage via error bodies.

Findings

# Severity File Line Finding
1 [MAJOR] .gitea/workflows/ci.yml 29 CI is failing (review jobs), likely due to missing or inaccessible AICORE_* secrets for pull_request runs. As per policy, any CI failure requires requesting changes.
2 [MINOR] llm/aicore.go 170 getDeploymentURL holds the write mutex while calling fetchDeployments, which performs network I/O. This can block other goroutines and creates a potential denial-of-service vector if the external request is slow (even with a timeout). Release the lock around network calls and only acquire it when updating the shared map.
3 [MINOR] llm/aicore.go 286 Error paths include the full response body in returned errors (e.g., "AI Core API error (status %d): %s"). If these errors are logged in CI or production, they may leak internal details. Consider truncating or redacting bodies in errors to reduce information exposure.

Recommendation

REQUEST_CHANGES — Address the CI failures first: ensure the review job has access to required AICORE_* secrets in pull_request context or guard the job (e.g., conditionally skip when secrets are unavailable, or use a non-blocking provider for PRs). For the AI Core client, refactor getDeploymentURL to avoid holding the write mutex during network I/O by performing fetchDeployments without the lock and then acquiring the lock only to update the deployments map. Additionally, reduce information leakage by avoiding inclusion of full response bodies in error messages (log status codes and concise messages, optionally truncating bodies). With these changes, the integration should be secure and robust.


Review by security


Evaluated against 5b3f6b1a

# Security Review ## Summary CI is failing for the PR, which requires changes before merge. The new SAP AI Core client and integration appear generally sound with no critical vulnerabilities, but there are minor hardening issues around lock usage during network I/O and potential information leakage via error bodies. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MAJOR] | `.gitea/workflows/ci.yml` | 29 | CI is failing (review jobs), likely due to missing or inaccessible AICORE_* secrets for pull_request runs. As per policy, any CI failure requires requesting changes. | | 2 | [MINOR] | `llm/aicore.go` | 170 | getDeploymentURL holds the write mutex while calling fetchDeployments, which performs network I/O. This can block other goroutines and creates a potential denial-of-service vector if the external request is slow (even with a timeout). Release the lock around network calls and only acquire it when updating the shared map. | | 3 | [MINOR] | `llm/aicore.go` | 286 | Error paths include the full response body in returned errors (e.g., "AI Core API error (status %d): %s"). If these errors are logged in CI or production, they may leak internal details. Consider truncating or redacting bodies in errors to reduce information exposure. | ## Recommendation **REQUEST_CHANGES** — Address the CI failures first: ensure the review job has access to required AICORE_* secrets in pull_request context or guard the job (e.g., conditionally skip when secrets are unavailable, or use a non-blocking provider for PRs). For the AI Core client, refactor getDeploymentURL to avoid holding the write mutex during network I/O by performing fetchDeployments without the lock and then acquiring the lock only to update the deployments map. Additionally, reduce information leakage by avoiding inclusion of full response bodies in error messages (log status codes and concise messages, optionally truncating bodies). With these changes, the integration should be secure and robust. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against 5b3f6b1a*
@@ -28,33 +29,21 @@ jobs:
include:
Collaborator

[MAJOR] CI is failing (review jobs), likely due to missing or inaccessible AICORE_* secrets for pull_request runs. As per policy, any CI failure requires requesting changes.

**[MAJOR]** CI is failing (review jobs), likely due to missing or inaccessible AICORE_* secrets for pull_request runs. As per policy, any CI failure requires requesting changes.
llm/aicore.go Outdated
@@ -0,0 +167,4 @@
func (c *AICoreClient) fetchDeployments(ctx context.Context, token string) error {
deployURL := strings.TrimRight(c.config.APIURL, "/") + "/v2/lm/deployments"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, deployURL, nil)
Collaborator

[MINOR] getDeploymentURL holds the write mutex while calling fetchDeployments, which performs network I/O. This can block other goroutines and creates a potential denial-of-service vector if the external request is slow (even with a timeout). Release the lock around network calls and only acquire it when updating the shared map.

**[MINOR]** getDeploymentURL holds the write mutex while calling fetchDeployments, which performs network I/O. This can block other goroutines and creates a potential denial-of-service vector if the external request is slow (even with a timeout). Release the lock around network calls and only acquire it when updating the shared map.
llm/aicore.go Outdated
@@ -0,0 +283,4 @@
return "", fmt.Errorf("AI Core request: %w", err)
}
defer resp.Body.Close()
Collaborator

[MINOR] Error paths include the full response body in returned errors (e.g., "AI Core API error (status %d): %s"). If these errors are logged in CI or production, they may leak internal details. Consider truncating or redacting bodies in errors to reduce information exposure.

**[MINOR]** Error paths include the full response body in returned errors (e.g., "AI Core API error (status %d): %s"). If these errors are logged in CI or production, they may leak internal details. Consider truncating or redacting bodies in errors to reduce information exposure.
sonnet-review-bot approved these changes 2026-05-10 08:10:19 +00:00
sonnet-review-bot left a comment
First-time contributor

Original review

Supersededsee current review for up-to-date findings.

Previous findings (commit 6d4c33a7)

Sonnet Review

Summary

This PR adds a well-structured SAP AI Core provider with proper OAuth2 token caching, deployment discovery, and comprehensive unit tests. The implementation follows existing patterns in the codebase and correctly handles both Anthropic and OpenAI models through AI Core. CI passes.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 228 CompleteAnthropic calls getDeploymentURL and then getToken as separate operations. Between these two calls, the token could theoretically expire (extremely unlikely in practice, but the token fetched for deployment discovery in getDeploymentURL is not reused here). This is a minor efficiency issue rather than a correctness bug since getToken handles caching and the window is tiny, but it would be slightly cleaner to fetch the token once and pass it through. Low priority given this only matters on token boundary.
2 [MINOR] llm/aicore.go 180 fetchDeployments is called while holding the write lock (c.mu.Lock() in getDeploymentURL). This means HTTP I/O (the deployments API call) happens under the mutex, which blocks all concurrent readers for the duration of the network round-trip. For a single-use CLI tool this is fine, but for a library it would be better to do the fetch outside the lock and then re-acquire to write. This is an accepted tradeoff for the current use case but worth noting.
3 [MINOR] llm/aicore.go 168 The deployments cache is populated once and never invalidated. If a deployment becomes unavailable (status changes from RUNNING to STOPPED) after initial discovery, the client will continue trying to use the old URL until the process restarts. Consider adding a TTL or a re-fetch on 4xx/5xx responses from the invoke endpoint. This is acceptable for a short-lived CI runner process but could be an issue for longer-running deployments.
4 [NIT] llm/aicore.go 37 The deployment struct has an ID field that is stored but never read anywhere in the codebase. If it's not used for anything, it adds unused memory per deployment entry. Either use it (e.g., for logging) or remove it.
5 [NIT] llm/aicore.go 30 The field name http shadows the net/http package name within the struct's methods. While Go resolves this correctly (the package is accessed via the import path, the field via the receiver), this is a common source of confusion. The existing llm/client.go uses the same pattern, so this is consistent with the codebase, but naming it httpClient would be clearer.
6 [NIT] llm/aicore_test.go 56 json.NewEncoder(w).Encode(...) errors are silently ignored throughout the test handlers. While this is common in test code and a failure here would manifest as a test failure, adding t.Helper() and checking the error would make test failures easier to diagnose.

Recommendation

APPROVE — Approve. The implementation is correct, well-tested, and consistent with the existing codebase patterns. The minor findings are mostly design tradeoffs that are acceptable for a short-lived CI runner process (no deployment cache invalidation, HTTP I/O under mutex). The unused deployment.ID field is the only change I'd encourage cleaning up before merge. No blocking issues.


Review by sonnet


Evaluated against 6d4c33a7

~~Original review~~ **Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/54#pullrequestreview-2022) for up-to-date findings. <details><summary>Previous findings (commit 6d4c33a7)</summary> # Sonnet Review ## Summary This PR adds a well-structured SAP AI Core provider with proper OAuth2 token caching, deployment discovery, and comprehensive unit tests. The implementation follows existing patterns in the codebase and correctly handles both Anthropic and OpenAI models through AI Core. CI passes. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 228 | CompleteAnthropic calls getDeploymentURL and then getToken as separate operations. Between these two calls, the token could theoretically expire (extremely unlikely in practice, but the token fetched for deployment discovery in getDeploymentURL is not reused here). This is a minor efficiency issue rather than a correctness bug since getToken handles caching and the window is tiny, but it would be slightly cleaner to fetch the token once and pass it through. Low priority given this only matters on token boundary. | | 2 | [MINOR] | `llm/aicore.go` | 180 | fetchDeployments is called while holding the write lock (c.mu.Lock() in getDeploymentURL). This means HTTP I/O (the deployments API call) happens under the mutex, which blocks all concurrent readers for the duration of the network round-trip. For a single-use CLI tool this is fine, but for a library it would be better to do the fetch outside the lock and then re-acquire to write. This is an accepted tradeoff for the current use case but worth noting. | | 3 | [MINOR] | `llm/aicore.go` | 168 | The deployments cache is populated once and never invalidated. If a deployment becomes unavailable (status changes from RUNNING to STOPPED) after initial discovery, the client will continue trying to use the old URL until the process restarts. Consider adding a TTL or a re-fetch on 4xx/5xx responses from the invoke endpoint. This is acceptable for a short-lived CI runner process but could be an issue for longer-running deployments. | | 4 | [NIT] | `llm/aicore.go` | 37 | The `deployment` struct has an `ID` field that is stored but never read anywhere in the codebase. If it's not used for anything, it adds unused memory per deployment entry. Either use it (e.g., for logging) or remove it. | | 5 | [NIT] | `llm/aicore.go` | 30 | The field name `http` shadows the `net/http` package name within the struct's methods. While Go resolves this correctly (the package is accessed via the import path, the field via the receiver), this is a common source of confusion. The existing `llm/client.go` uses the same pattern, so this is consistent with the codebase, but naming it `httpClient` would be clearer. | | 6 | [NIT] | `llm/aicore_test.go` | 56 | json.NewEncoder(w).Encode(...) errors are silently ignored throughout the test handlers. While this is common in test code and a failure here would manifest as a test failure, adding t.Helper() and checking the error would make test failures easier to diagnose. | ## Recommendation **APPROVE** — Approve. The implementation is correct, well-tested, and consistent with the existing codebase patterns. The minor findings are mostly design tradeoffs that are acceptable for a short-lived CI runner process (no deployment cache invalidation, HTTP I/O under mutex). The unused `deployment.ID` field is the only change I'd encourage cleaning up before merge. No blocking issues. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 6d4c33a7* </details> <!-- review-bot:sonnet -->
llm/aicore.go Outdated
@@ -0,0 +27,4 @@
}
// AICoreClient wraps AI Core authentication and deployment discovery.
// Thread-safe for concurrent use after construction.
First-time contributor

[NIT] The field name http shadows the net/http package name within the struct's methods. While Go resolves this correctly (the package is accessed via the import path, the field via the receiver), this is a common source of confusion. The existing llm/client.go uses the same pattern, so this is consistent with the codebase, but naming it httpClient would be clearer.

**[NIT]** The field name `http` shadows the `net/http` package name within the struct's methods. While Go resolves this correctly (the package is accessed via the import path, the field via the receiver), this is a common source of confusion. The existing `llm/client.go` uses the same pattern, so this is consistent with the codebase, but naming it `httpClient` would be clearer.
llm/aicore.go Outdated
@@ -0,0 +34,4 @@
mu sync.RWMutex
token string
tokenExpiry time.Time
First-time contributor

[NIT] The deployment struct has an ID field that is stored but never read anywhere in the codebase. If it's not used for anything, it adds unused memory per deployment entry. Either use it (e.g., for logging) or remove it.

**[NIT]** The `deployment` struct has an `ID` field that is stored but never read anywhere in the codebase. If it's not used for anything, it adds unused memory per deployment entry. Either use it (e.g., for logging) or remove it.
llm/aicore.go Outdated
@@ -0,0 +165,4 @@
return "", fmt.Errorf("no deployment found for model %q", model)
}
func (c *AICoreClient) fetchDeployments(ctx context.Context, token string) error {
First-time contributor

[MINOR] The deployments cache is populated once and never invalidated. If a deployment becomes unavailable (status changes from RUNNING to STOPPED) after initial discovery, the client will continue trying to use the old URL until the process restarts. Consider adding a TTL or a re-fetch on 4xx/5xx responses from the invoke endpoint. This is acceptable for a short-lived CI runner process but could be an issue for longer-running deployments.

**[MINOR]** The deployments cache is populated once and never invalidated. If a deployment becomes unavailable (status changes from RUNNING to STOPPED) after initial discovery, the client will continue trying to use the old URL until the process restarts. Consider adding a TTL or a re-fetch on 4xx/5xx responses from the invoke endpoint. This is acceptable for a short-lived CI runner process but could be an issue for longer-running deployments.
llm/aicore.go Outdated
@@ -0,0 +177,4 @@
resp, err := c.http.Do(req)
if err != nil {
return fmt.Errorf("deployments request: %w", err)
}
First-time contributor

[MINOR] fetchDeployments is called while holding the write lock (c.mu.Lock() in getDeploymentURL). This means HTTP I/O (the deployments API call) happens under the mutex, which blocks all concurrent readers for the duration of the network round-trip. For a single-use CLI tool this is fine, but for a library it would be better to do the fetch outside the lock and then re-acquire to write. This is an accepted tradeoff for the current use case but worth noting.

**[MINOR]** fetchDeployments is called while holding the write lock (c.mu.Lock() in getDeploymentURL). This means HTTP I/O (the deployments API call) happens under the mutex, which blocks all concurrent readers for the duration of the network round-trip. For a single-use CLI tool this is fine, but for a library it would be better to do the fetch outside the lock and then re-acquire to write. This is an accepted tradeoff for the current use case but worth noting.
llm/aicore.go Outdated
@@ -0,0 +225,4 @@
return nil
}
First-time contributor

[MINOR] CompleteAnthropic calls getDeploymentURL and then getToken as separate operations. Between these two calls, the token could theoretically expire (extremely unlikely in practice, but the token fetched for deployment discovery in getDeploymentURL is not reused here). This is a minor efficiency issue rather than a correctness bug since getToken handles caching and the window is tiny, but it would be slightly cleaner to fetch the token once and pass it through. Low priority given this only matters on token boundary.

**[MINOR]** CompleteAnthropic calls getDeploymentURL and then getToken as separate operations. Between these two calls, the token could theoretically expire (extremely unlikely in practice, but the token fetched for deployment discovery in getDeploymentURL is not reused here). This is a minor efficiency issue rather than a correctness bug since getToken handles caching and the window is tiny, but it would be slightly cleaner to fetch the token once and pass it through. Low priority given this only matters on token boundary.
@@ -0,0 +53,4 @@
// Second call should use cached token
token2, err := client.getToken(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
First-time contributor

[NIT] json.NewEncoder(w).Encode(...) errors are silently ignored throughout the test handlers. While this is common in test code and a failure here would manifest as a test failure, adding t.Helper() and checking the error would make test failures easier to diagnose.

**[NIT]** json.NewEncoder(w).Encode(...) errors are silently ignored throughout the test handlers. While this is common in test code and a failure here would manifest as a test failure, adding t.Helper() and checking the error would make test failures easier to diagnose.
aweiker approved these changes 2026-05-10 08:10:25 +00:00
Dismissed
gpt-review-bot approved these changes 2026-05-10 08:10:48 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Original review

Supersededsee current review for up-to-date findings.

Previous findings (commit 6d4c33a7)

Gpt Review

Summary

Native SAP AI Core support is cleanly integrated across the CLI, LLM client, CI, and docs. The AICore client appears concurrency-safe, handles token refresh and deployment discovery, and is covered by solid unit tests. CI passed and overall changes are consistent and idiomatic.

Recommendation

APPROVE — The implementation adheres to repository conventions (stdlib only, error wrapping, tests) and Go patterns (zero-value ready clients, concurrency safety, clear config). The action and workflow updates align with the new provider and documentation is updated accordingly. Consider monitoring production behavior for any AI Core-specific nuances (e.g., future API version changes, model/deployment mapping) and extending retry logic for specific transient HTTP status codes if needed, but no changes are required for this PR.


Review by gpt


Evaluated against 6d4c33a7

~~Original review~~ **Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/54#pullrequestreview-2025) for up-to-date findings. <details><summary>Previous findings (commit 6d4c33a7)</summary> # Gpt Review ## Summary Native SAP AI Core support is cleanly integrated across the CLI, LLM client, CI, and docs. The AICore client appears concurrency-safe, handles token refresh and deployment discovery, and is covered by solid unit tests. CI passed and overall changes are consistent and idiomatic. ## Recommendation **APPROVE** — The implementation adheres to repository conventions (stdlib only, error wrapping, tests) and Go patterns (zero-value ready clients, concurrency safety, clear config). The action and workflow updates align with the new provider and documentation is updated accordingly. Consider monitoring production behavior for any AI Core-specific nuances (e.g., future API version changes, model/deployment mapping) and extending retry logic for specific transient HTTP status codes if needed, but no changes are required for this PR. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 6d4c33a7* </details> <!-- review-bot:gpt -->
security-review-bot requested review from security-review-bot 2026-05-10 08:11:06 +00:00
security-review-bot approved these changes 2026-05-10 08:11:06 +00:00
Dismissed
security-review-bot left a comment
Collaborator

Security Review

Summary

The changes add native SAP AI Core support with OAuth2, deployment discovery, and provider integration. CI has passed, and the implementation appears sound with no exploitable vulnerabilities identified. A couple of minor hardening improvements are suggested.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 87 AICore AuthURL and APIURL are accepted as-is and used to construct requests without enforcing HTTPS. If misconfigured to use plain HTTP, client credentials could be sent in cleartext. Consider validating the scheme and rejecting non-HTTPS URLs or warning explicitly.
2 [MINOR] llm/aicore.go 112 Errors include the full upstream response body (e.g., token/deployments/invoke failures). Upstream error bodies may echo user prompts or internal details; propagating these into logs can leak sensitive data. Consider truncating/redacting bodies in errors or logging only status codes and minimal context.

Recommendation

APPROVE — Overall, the AI Core provider integration is well-structured: credentials are not logged, OAuth tokens are cached with thread-safe refresh, and request timeouts are set. CI has passed. For defense-in-depth, please consider (1) validating that AICore AuthURL and APIURL use HTTPS to avoid accidental plaintext transmission of client credentials, and (2) avoiding inclusion of full upstream response bodies in returned errors to reduce the risk of sensitive information leakage in logs. With these minor hardening changes, the implementation would be even more robust.


Review by security


Evaluated against 6d4c33a7

# Security Review ## Summary The changes add native SAP AI Core support with OAuth2, deployment discovery, and provider integration. CI has passed, and the implementation appears sound with no exploitable vulnerabilities identified. A couple of minor hardening improvements are suggested. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 87 | AICore AuthURL and APIURL are accepted as-is and used to construct requests without enforcing HTTPS. If misconfigured to use plain HTTP, client credentials could be sent in cleartext. Consider validating the scheme and rejecting non-HTTPS URLs or warning explicitly. | | 2 | [MINOR] | `llm/aicore.go` | 112 | Errors include the full upstream response body (e.g., token/deployments/invoke failures). Upstream error bodies may echo user prompts or internal details; propagating these into logs can leak sensitive data. Consider truncating/redacting bodies in errors or logging only status codes and minimal context. | ## Recommendation **APPROVE** — Overall, the AI Core provider integration is well-structured: credentials are not logged, OAuth tokens are cached with thread-safe refresh, and request timeouts are set. CI has passed. For defense-in-depth, please consider (1) validating that AICore AuthURL and APIURL use HTTPS to avoid accidental plaintext transmission of client credentials, and (2) avoiding inclusion of full upstream response bodies in returned errors to reduce the risk of sensitive information leakage in logs. With these minor hardening changes, the implementation would be even more robust. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against 6d4c33a7*
llm/aicore.go Outdated
@@ -0,0 +84,4 @@
}
c.token = token
c.tokenExpiry = expiry
return token, nil
Collaborator

[MINOR] AICore AuthURL and APIURL are accepted as-is and used to construct requests without enforcing HTTPS. If misconfigured to use plain HTTP, client credentials could be sent in cleartext. Consider validating the scheme and rejecting non-HTTPS URLs or warning explicitly.

**[MINOR]** AICore AuthURL and APIURL are accepted as-is and used to construct requests without enforcing HTTPS. If misconfigured to use plain HTTP, client credentials could be sent in cleartext. Consider validating the scheme and rejecting non-HTTPS URLs or warning explicitly.
llm/aicore.go Outdated
@@ -0,0 +109,4 @@
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", time.Time{}, fmt.Errorf("read token response: %w", err)
Collaborator

[MINOR] Errors include the full upstream response body (e.g., token/deployments/invoke failures). Upstream error bodies may echo user prompts or internal details; propagating these into logs can leak sensitive data. Consider truncating/redacting bodies in errors or logging only status codes and minimal context.

**[MINOR]** Errors include the full upstream response body (e.g., token/deployments/invoke failures). Upstream error bodies may echo user prompts or internal details; propagating these into logs can leak sensitive data. Consider truncating/redacting bodies in errors or logging only status codes and minimal context.
Author
Owner

Self-Review Assessment

Reviewed findings from sonnet, gpt, and security bots. Here is my assessment:

Changes Made (commit 3276496)

  1. Security: Response body truncation — Added truncateBody helper (200 char limit) applied to all error paths that include upstream response bodies. This addresses the security bot finding about information leakage in logs.

  2. Cleanup: Removed unused deployment.ID field — The struct now just stores the deployment URL string directly. Cleaner and removes dead storage.

  3. Documentation: Cache limitation note — Added doc comment explaining that deployment cache is never invalidated, documenting this as acceptable for short-lived CI use case.

  4. NIT: Trailing whitespace — Fixed in action.yml aicore-resource-group default.

Findings NOT Addressed (Acceptable)

Finding Assessment
HTTPS enforcement on URLs Low risk — AI Core URLs come from Gitea secrets, misconfiguration would be an operator error. Could add validation but not blocking.
Mutex during fetchDeployments Already mitigated — token is fetched BEFORE write lock. The remaining I/O under lock is one-time initialization. Acceptable for CLI tool.
Deployment cache never invalidated Documented as known limitation. Acceptable for CI runner use case.
Field http shadows package Consistent with existing llm/client.go pattern. Changing would be inconsistent.
Test encoder errors ignored Test-only, failures would manifest as test failures.

Summary

All MAJOR findings addressed. MINOR security finding (body truncation) addressed. NITs resolved where impactful. Tests pass, build succeeds.

## Self-Review Assessment Reviewed findings from sonnet, gpt, and security bots. Here is my assessment: ### Changes Made (commit 3276496) 1. **Security: Response body truncation** — Added `truncateBody` helper (200 char limit) applied to all error paths that include upstream response bodies. This addresses the security bot finding about information leakage in logs. 2. **Cleanup: Removed unused `deployment.ID` field** — The struct now just stores the deployment URL string directly. Cleaner and removes dead storage. 3. **Documentation: Cache limitation note** — Added doc comment explaining that deployment cache is never invalidated, documenting this as acceptable for short-lived CI use case. 4. **NIT: Trailing whitespace** — Fixed in action.yml aicore-resource-group default. ### Findings NOT Addressed (Acceptable) | Finding | Assessment | |---------|------------| | HTTPS enforcement on URLs | Low risk — AI Core URLs come from Gitea secrets, misconfiguration would be an operator error. Could add validation but not blocking. | | Mutex during fetchDeployments | Already mitigated — token is fetched BEFORE write lock. The remaining I/O under lock is one-time initialization. Acceptable for CLI tool. | | Deployment cache never invalidated | Documented as known limitation. Acceptable for CI runner use case. | | Field `http` shadows package | Consistent with existing `llm/client.go` pattern. Changing would be inconsistent. | | Test encoder errors ignored | Test-only, failures would manifest as test failures. | ### Summary All MAJOR findings addressed. MINOR security finding (body truncation) addressed. NITs resolved where impactful. Tests pass, build succeeds.
rodin added the self-reviewed label 2026-05-10 08:26:38 +00:00
sonnet-review-bot approved these changes 2026-05-10 08:27:10 +00:00
sonnet-review-bot left a comment
First-time contributor

Sonnet Review

Summary

This PR adds a well-structured SAP AI Core provider with proper OAuth2 token caching, deployment discovery, and routing between Anthropic and OpenAI models. The implementation follows Go conventions, includes comprehensive unit tests using httptest, and the CI passes.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 184 The client_secret is sent as a URL-encoded form parameter (data.Set("client_secret", c.config.ClientSecret)). While this is valid per OAuth2 spec (Resource Owner Password flow uses form body), many OAuth2 servers also accept/prefer HTTP Basic auth (Authorization: Basic base64(clientID:secret)). The current approach is spec-compliant but worth noting as some IdPs may reject it. Not a bug, but the comment could document why form body is used over HTTP Basic auth for this particular IdP.
2 [MINOR] llm/aicore.go 199 When tokenResp.ExpiresIn is 0 (either because the field is missing or the server returns 0), expiry will equal time.Now(), causing every subsequent call to immediately re-fetch the token. A reasonable fallback (e.g., if tokenResp.ExpiresIn <= 0 { tokenResp.ExpiresIn = 3600 }) would be more robust.
3 [MINOR] llm/aicore.go 247 If two concurrent goroutines both miss the read-lock cache check for getDeploymentURL, they will both call getToken before acquiring the write lock. Since getToken itself acquires c.mu internally, and then getDeploymentURL acquires c.mu.Lock(), there's a potential for the two goroutines to acquire c.mu.Lock() sequentially, both calling fetchDeployments. The double-check after acquiring the write lock prevents duplicate work, but the second goroutine still called getToken unnecessarily. This is a minor efficiency concern, not a correctness issue, since getToken is also protected by the same mutex and has its own double-check.
4 [NIT] llm/aicore.go 303 The AnthropicVersion field is hardcoded as a string literal "bedrock-2023-05-31" inside CompleteAnthropic. Consider extracting this as a named constant (e.g. AICoreAnthropicVersion) similar to AICoreOpenAIAPIVersion, for consistency and to make it easy to update.
5 [NIT] llm/aicore_test.go 35 The test uses json.NewEncoder(w).Encode(...) without checking its error return. While errors from writing to an httptest.ResponseRecorder are extremely rare, this is technically ignoring an error. The established project convention is to check all error returns.
6 [NIT] llm/aicore.go 159 The variable url (used in if url, ok := c.deployments[model]; ok) shadows the built-in net/url package import, but net/url is imported and only used in fetchToken. While it works, renaming the local variable (e.g., deployURL) would avoid the shadow and make the code clearer.

Recommendation

APPROVE — Approve. The implementation is correct, well-tested, and follows established Go patterns. The minor findings are non-critical: the ExpiresIn == 0 edge case is the most worth addressing (prevents a token-thrashing scenario with misconfigured IdPs), and extracting the Bedrock version string as a constant mirrors the existing AICoreOpenAIAPIVersion pattern. None of these block merging.


Review by sonnet


Evaluated against 32764960

# Sonnet Review ## Summary This PR adds a well-structured SAP AI Core provider with proper OAuth2 token caching, deployment discovery, and routing between Anthropic and OpenAI models. The implementation follows Go conventions, includes comprehensive unit tests using httptest, and the CI passes. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 184 | The client_secret is sent as a URL-encoded form parameter (`data.Set("client_secret", c.config.ClientSecret)`). While this is valid per OAuth2 spec (Resource Owner Password flow uses form body), many OAuth2 servers also accept/prefer HTTP Basic auth (`Authorization: Basic base64(clientID:secret)`). The current approach is spec-compliant but worth noting as some IdPs may reject it. Not a bug, but the comment could document why form body is used over HTTP Basic auth for this particular IdP. | | 2 | [MINOR] | `llm/aicore.go` | 199 | When `tokenResp.ExpiresIn` is 0 (either because the field is missing or the server returns 0), `expiry` will equal `time.Now()`, causing every subsequent call to immediately re-fetch the token. A reasonable fallback (e.g., `if tokenResp.ExpiresIn <= 0 { tokenResp.ExpiresIn = 3600 }`) would be more robust. | | 3 | [MINOR] | `llm/aicore.go` | 247 | If two concurrent goroutines both miss the read-lock cache check for `getDeploymentURL`, they will both call `getToken` before acquiring the write lock. Since `getToken` itself acquires `c.mu` internally, and then `getDeploymentURL` acquires `c.mu.Lock()`, there's a potential for the two goroutines to acquire `c.mu.Lock()` sequentially, both calling `fetchDeployments`. The double-check after acquiring the write lock prevents duplicate work, but the second goroutine still called `getToken` unnecessarily. This is a minor efficiency concern, not a correctness issue, since `getToken` is also protected by the same mutex and has its own double-check. | | 4 | [NIT] | `llm/aicore.go` | 303 | The `AnthropicVersion` field is hardcoded as a string literal `"bedrock-2023-05-31"` inside `CompleteAnthropic`. Consider extracting this as a named constant (e.g. `AICoreAnthropicVersion`) similar to `AICoreOpenAIAPIVersion`, for consistency and to make it easy to update. | | 5 | [NIT] | `llm/aicore_test.go` | 35 | The test uses `json.NewEncoder(w).Encode(...)` without checking its error return. While errors from writing to an `httptest.ResponseRecorder` are extremely rare, this is technically ignoring an error. The established project convention is to check all error returns. | | 6 | [NIT] | `llm/aicore.go` | 159 | The variable `url` (used in `if url, ok := c.deployments[model]; ok`) shadows the built-in `net/url` package import, but `net/url` is imported and only used in `fetchToken`. While it works, renaming the local variable (e.g., `deployURL`) would avoid the shadow and make the code clearer. | ## Recommendation **APPROVE** — Approve. The implementation is correct, well-tested, and follows established Go patterns. The minor findings are non-critical: the `ExpiresIn == 0` edge case is the most worth addressing (prevents a token-thrashing scenario with misconfigured IdPs), and extracting the Bedrock version string as a constant mirrors the existing `AICoreOpenAIAPIVersion` pattern. None of these block merging. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 32764960*
llm/aicore.go Outdated
@@ -0,0 +156,4 @@
// Fetch token first (before acquiring write lock to avoid deadlock)
token, err := c.getToken(ctx)
if err != nil {
return "", fmt.Errorf("get token for deployments: %w", err)
First-time contributor

[NIT] The variable url (used in if url, ok := c.deployments[model]; ok) shadows the built-in net/url package import, but net/url is imported and only used in fetchToken. While it works, renaming the local variable (e.g., deployURL) would avoid the shadow and make the code clearer.

**[NIT]** The variable `url` (used in `if url, ok := c.deployments[model]; ok`) shadows the built-in `net/url` package import, but `net/url` is imported and only used in `fetchToken`. While it works, renaming the local variable (e.g., `deployURL`) would avoid the shadow and make the code clearer.
llm/aicore.go Outdated
@@ -0,0 +181,4 @@
deployURL := strings.TrimRight(c.config.APIURL, "/") + "/v2/lm/deployments"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, deployURL, nil)
if err != nil {
return fmt.Errorf("create deployments request: %w", err)
First-time contributor

[MINOR] The client_secret is sent as a URL-encoded form parameter (data.Set("client_secret", c.config.ClientSecret)). While this is valid per OAuth2 spec (Resource Owner Password flow uses form body), many OAuth2 servers also accept/prefer HTTP Basic auth (Authorization: Basic base64(clientID:secret)). The current approach is spec-compliant but worth noting as some IdPs may reject it. Not a bug, but the comment could document why form body is used over HTTP Basic auth for this particular IdP.

**[MINOR]** The client_secret is sent as a URL-encoded form parameter (`data.Set("client_secret", c.config.ClientSecret)`). While this is valid per OAuth2 spec (Resource Owner Password flow uses form body), many OAuth2 servers also accept/prefer HTTP Basic auth (`Authorization: Basic base64(clientID:secret)`). The current approach is spec-compliant but worth noting as some IdPs may reject it. Not a bug, but the comment could document why form body is used over HTTP Basic auth for this particular IdP.
llm/aicore.go Outdated
@@ -0,0 +196,4 @@
if err != nil {
return fmt.Errorf("read deployments response: %w", err)
}
First-time contributor

[MINOR] When tokenResp.ExpiresIn is 0 (either because the field is missing or the server returns 0), expiry will equal time.Now(), causing every subsequent call to immediately re-fetch the token. A reasonable fallback (e.g., if tokenResp.ExpiresIn <= 0 { tokenResp.ExpiresIn = 3600 }) would be more robust.

**[MINOR]** When `tokenResp.ExpiresIn` is 0 (either because the field is missing or the server returns 0), `expiry` will equal `time.Now()`, causing every subsequent call to immediately re-fetch the token. A reasonable fallback (e.g., `if tokenResp.ExpiresIn <= 0 { tokenResp.ExpiresIn = 3600 }`) would be more robust.
llm/aicore.go Outdated
@@ -0,0 +244,4 @@
token, err := c.getToken(ctx)
if err != nil {
return "", err
}
First-time contributor

[MINOR] If two concurrent goroutines both miss the read-lock cache check for getDeploymentURL, they will both call getToken before acquiring the write lock. Since getToken itself acquires c.mu internally, and then getDeploymentURL acquires c.mu.Lock(), there's a potential for the two goroutines to acquire c.mu.Lock() sequentially, both calling fetchDeployments. The double-check after acquiring the write lock prevents duplicate work, but the second goroutine still called getToken unnecessarily. This is a minor efficiency concern, not a correctness issue, since getToken is also protected by the same mutex and has its own double-check.

**[MINOR]** If two concurrent goroutines both miss the read-lock cache check for `getDeploymentURL`, they will both call `getToken` before acquiring the write lock. Since `getToken` itself acquires `c.mu` internally, and then `getDeploymentURL` acquires `c.mu.Lock()`, there's a potential for the two goroutines to acquire `c.mu.Lock()` sequentially, both calling `fetchDeployments`. The double-check after acquiring the write lock prevents duplicate work, but the second goroutine still called `getToken` unnecessarily. This is a minor efficiency concern, not a correctness issue, since `getToken` is also protected by the same mutex and has its own double-check.
llm/aicore.go Outdated
@@ -0,0 +300,4 @@
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("AI Core API error (status %d): %s", resp.StatusCode, truncateBody(body))
}
First-time contributor

[NIT] The AnthropicVersion field is hardcoded as a string literal "bedrock-2023-05-31" inside CompleteAnthropic. Consider extracting this as a named constant (e.g. AICoreAnthropicVersion) similar to AICoreOpenAIAPIVersion, for consistency and to make it easy to update.

**[NIT]** The `AnthropicVersion` field is hardcoded as a string literal `"bedrock-2023-05-31"` inside `CompleteAnthropic`. Consider extracting this as a named constant (e.g. `AICoreAnthropicVersion`) similar to `AICoreOpenAIAPIVersion`, for consistency and to make it easy to update.
@@ -0,0 +32,4 @@
}
t.Errorf("unexpected path: %s", r.URL.Path)
}))
defer server.Close()
First-time contributor

[NIT] The test uses json.NewEncoder(w).Encode(...) without checking its error return. While errors from writing to an httptest.ResponseRecorder are extremely rare, this is technically ignoring an error. The established project convention is to check all error returns.

**[NIT]** The test uses `json.NewEncoder(w).Encode(...)` without checking its error return. While errors from writing to an `httptest.ResponseRecorder` are extremely rare, this is technically ignoring an error. The established project convention is to check all error returns.
security-review-bot requested review from security-review-bot 2026-05-10 08:28:15 +00:00
security-review-bot approved these changes 2026-05-10 08:28:16 +00:00
Dismissed
security-review-bot left a comment
Collaborator

Security Review

Summary

The PR cleanly adds native SAP AI Core support with OAuth2, deployment discovery, and provider wiring. Security-conscious choices like truncating upstream error bodies and enforcing timeouts are present. I did not find exploitable issues; a few hardening suggestions are noted.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 20 AICore endpoints (AuthURL/APIURL) are taken as-is without validating scheme; consider enforcing HTTPS to avoid accidental plaintext credential transmission or SSRF to non-HTTPS endpoints.
2 [MINOR] llm/aicore.go 96 All AI Core responses are read with io.ReadAll without a size cap. For defense-in-depth against oversized or malfunctioning endpoints, consider limiting reads (e.g., io.LimitedReader) especially on error paths to mitigate memory exhaustion.
3 [NIT] cmd/review-bot/main.go 92 When provider=aicore, credentials are validated but URLs are not. Adding a simple URL parse and HTTPS enforcement (or a clear warning) would reduce misconfiguration risk and ensure secure transport.

Recommendation

APPROVE — Overall, the implementation is sound and integrates SAP AI Core without exposing secrets or introducing obvious vulnerabilities. To further harden the new provider: (1) validate and enforce HTTPS for AICORE_AUTH_URL and AICORE_API_URL before use; (2) cap response body sizes on AI Core HTTP reads to avoid potential memory exhaustion on error or misbehaving endpoints. With these small improvements, the security posture will be stronger. Since CI is passing and no major issues were found, this is approved.


Review by security


Evaluated against 32764960

# Security Review ## Summary The PR cleanly adds native SAP AI Core support with OAuth2, deployment discovery, and provider wiring. Security-conscious choices like truncating upstream error bodies and enforcing timeouts are present. I did not find exploitable issues; a few hardening suggestions are noted. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 20 | AICore endpoints (AuthURL/APIURL) are taken as-is without validating scheme; consider enforcing HTTPS to avoid accidental plaintext credential transmission or SSRF to non-HTTPS endpoints. | | 2 | [MINOR] | `llm/aicore.go` | 96 | All AI Core responses are read with io.ReadAll without a size cap. For defense-in-depth against oversized or malfunctioning endpoints, consider limiting reads (e.g., io.LimitedReader) especially on error paths to mitigate memory exhaustion. | | 3 | [NIT] | `cmd/review-bot/main.go` | 92 | When provider=aicore, credentials are validated but URLs are not. Adding a simple URL parse and HTTPS enforcement (or a clear warning) would reduce misconfiguration risk and ensure secure transport. | ## Recommendation **APPROVE** — Overall, the implementation is sound and integrates SAP AI Core without exposing secrets or introducing obvious vulnerabilities. To further harden the new provider: (1) validate and enforce HTTPS for AICORE_AUTH_URL and AICORE_API_URL before use; (2) cap response body sizes on AI Core HTTP reads to avoid potential memory exhaustion on error or misbehaving endpoints. With these small improvements, the security posture will be stronger. Since CI is passing and no major issues were found, this is approved. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against 32764960*
@@ -84,10 +90,20 @@ func main() {
slog.Info("review-bot starting", "version", version)
// Validate required fields
Collaborator

[NIT] When provider=aicore, credentials are validated but URLs are not. Adding a simple URL parse and HTTPS enforcement (or a clear warning) would reduce misconfiguration risk and ensure secure transport.

**[NIT]** When provider=aicore, credentials are validated but URLs are not. Adding a simple URL parse and HTTPS enforcement (or a clear warning) would reduce misconfiguration risk and ensure secure transport.
llm/aicore.go Outdated
@@ -0,0 +17,4 @@
// Update this when SAP AI Core releases a new stable version.
const AICoreOpenAIAPIVersion = "2024-12-01-preview"
// maxErrorBodyLen limits the length of response bodies included in error messages
Collaborator

[MINOR] AICore endpoints (AuthURL/APIURL) are taken as-is without validating scheme; consider enforcing HTTPS to avoid accidental plaintext credential transmission or SSRF to non-HTTPS endpoints.

**[MINOR]** AICore endpoints (AuthURL/APIURL) are taken as-is without validating scheme; consider enforcing HTTPS to avoid accidental plaintext credential transmission or SSRF to non-HTTPS endpoints.
llm/aicore.go Outdated
@@ -0,0 +93,4 @@
token, expiry, err := c.fetchToken(ctx)
if err != nil {
return "", err
}
Collaborator

[MINOR] All AI Core responses are read with io.ReadAll without a size cap. For defense-in-depth against oversized or malfunctioning endpoints, consider limiting reads (e.g., io.LimitedReader) especially on error paths to mitigate memory exhaustion.

**[MINOR]** All AI Core responses are read with io.ReadAll without a size cap. For defense-in-depth against oversized or malfunctioning endpoints, consider limiting reads (e.g., io.LimitedReader) especially on error paths to mitigate memory exhaustion.
gpt-review-bot approved these changes 2026-05-10 08:28:46 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Gpt Review

Summary

Well-structured addition of native SAP AI Core support with clear flag/env wiring, provider selection, and thorough unit tests. Implementation is idiomatic and secure (token refresh, truncated error bodies), with only minor design nits.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 145 fetchDeployments is invoked while holding the write lock in getDeploymentURL, causing a network request under mutex. This can block other goroutines (token refresh or cached reads) longer than necessary. Consider fetching deployments outside the lock and updating the cache under the lock, or using a separate mutex for deployments vs token.
2 [NIT] llm/aicore.go 260 CompleteOpenAI includes the "model" field in the request body even though SAP AI Core deployments already determine the model. While likely harmless, omitting it could reduce confusion or potential mismatch if the payload model name diverges from the deployment.

Recommendation

APPROVE — The PR cleanly integrates SAP AI Core as a first-class provider: inputs and CI are updated appropriately, CLI validation accounts for provider differences, and the LLM client introduces a dedicated AI Core path with proper token management and deployment discovery. Unit tests thoroughly cover token caching/expiry, deployment selection, and both Anthropic and OpenAI-style invocations. Consider a small concurrency improvement in AICoreClient.getDeploymentURL by avoiding holding the write lock during network requests, and optionally omit the model field in AI Core OpenAI payloads to avoid confusion. Otherwise, the changes look solid — approve.


Review by gpt


Evaluated against 32764960

# Gpt Review ## Summary Well-structured addition of native SAP AI Core support with clear flag/env wiring, provider selection, and thorough unit tests. Implementation is idiomatic and secure (token refresh, truncated error bodies), with only minor design nits. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 145 | fetchDeployments is invoked while holding the write lock in getDeploymentURL, causing a network request under mutex. This can block other goroutines (token refresh or cached reads) longer than necessary. Consider fetching deployments outside the lock and updating the cache under the lock, or using a separate mutex for deployments vs token. | | 2 | [NIT] | `llm/aicore.go` | 260 | CompleteOpenAI includes the "model" field in the request body even though SAP AI Core deployments already determine the model. While likely harmless, omitting it could reduce confusion or potential mismatch if the payload model name diverges from the deployment. | ## Recommendation **APPROVE** — The PR cleanly integrates SAP AI Core as a first-class provider: inputs and CI are updated appropriately, CLI validation accounts for provider differences, and the LLM client introduces a dedicated AI Core path with proper token management and deployment discovery. Unit tests thoroughly cover token caching/expiry, deployment selection, and both Anthropic and OpenAI-style invocations. Consider a small concurrency improvement in AICoreClient.getDeploymentURL by avoiding holding the write lock during network requests, and optionally omit the model field in AI Core OpenAI payloads to avoid confusion. Otherwise, the changes look solid — approve. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 32764960*
llm/aicore.go Outdated
@@ -0,0 +142,4 @@
expiry := time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
return tokenResp.AccessToken, expiry, nil
}
First-time contributor

[MINOR] fetchDeployments is invoked while holding the write lock in getDeploymentURL, causing a network request under mutex. This can block other goroutines (token refresh or cached reads) longer than necessary. Consider fetching deployments outside the lock and updating the cache under the lock, or using a separate mutex for deployments vs token.

**[MINOR]** fetchDeployments is invoked while holding the write lock in getDeploymentURL, causing a network request under mutex. This can block other goroutines (token refresh or cached reads) longer than necessary. Consider fetching deployments outside the lock and updating the cache under the lock, or using a separate mutex for deployments vs token.
llm/aicore.go Outdated
@@ -0,0 +257,4 @@
Role: m.Role,
Content: m.Content,
})
}
First-time contributor

[NIT] CompleteOpenAI includes the "model" field in the request body even though SAP AI Core deployments already determine the model. While likely harmless, omitting it could reduce confusion or potential mismatch if the payload model name diverges from the deployment.

**[NIT]** CompleteOpenAI includes the "model" field in the request body even though SAP AI Core deployments already determine the model. While likely harmless, omitting it could reduce confusion or potential mismatch if the payload model name diverges from the deployment.
rodin removed their assignment 2026-05-10 08:36:02 +00:00
aweiker was assigned by rodin 2026-05-10 08:36:02 +00:00
rodin force-pushed feat/aicore-provider-v2 from 327649606b to 5132a2028d 2026-05-10 15:29:18 +00:00 Compare
rodin force-pushed feat/aicore-provider-v2 from 5132a2028d to 03a26a4f8c 2026-05-10 15:30:03 +00:00 Compare
sonnet-review-bot approved these changes 2026-05-10 15:30:56 +00:00
sonnet-review-bot left a comment
First-time contributor

Sonnet Review

Summary

This PR adds a well-implemented native SAP AI Core provider with OAuth2 token caching, deployment discovery, and routing to both Anthropic and OpenAI endpoints. The code follows Go idioms, is well-tested with httptest, and CI passes. There are a few minor issues worth noting but nothing blocking.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 221 In CompleteAnthropic, the token is fetched after getDeploymentURL which itself calls getToken internally. This results in two calls to getToken — one inside getDeploymentURL and one explicit call afterwards. The second call will typically be a cheap cache hit, but it's redundant and slightly misleading. Consider using the token returned from getDeploymentURL by refactoring, or at minimum add a comment explaining why the second fetch is safe/cheap.
2 [MINOR] llm/aicore.go 280 Same double getToken pattern in CompleteOpenAIgetDeploymentURL already fetches a token internally, then another explicit getToken call follows. Same concern as above.
3 [MINOR] llm/aicore.go 190 When multiple goroutines concurrently call getDeploymentURL for different models that are both missing from the cache, they will all serialize on the write lock and call fetchDeployments once (due to the double-check). However, if the first fetchDeployments populates only some models and a concurrent caller for a different model races past the first read-lock check but before the write-lock check, behavior is correct. This is fine. But there's a subtle issue: the token fetched before acquiring the write lock in getDeploymentURL could expire by the time fetchDeployments uses it (especially with a short-lived token). Since the code holds getToken(ctx) result and passes it to fetchDeployments, but fetchDeployments is called potentially after a long wait for the write lock, the token might be expired. Given the 5-minute refresh buffer this is extremely unlikely in practice, but worth documenting.
4 [MINOR] llm/aicore.go 194 The getDeploymentURL function shadows the builtin url identifier from the net/url package imported at the top of the file. Since net/url is only used in fetchToken and not in getDeploymentURL, this shadowing doesn't cause a bug, but it is slightly surprising. Consider renaming the local variable to deployURL to be consistent with the naming used elsewhere and avoid the shadow.
5 [NIT] llm/aicore.go 47 The AICoreClient struct doc comment references 'issue #54 review discussion' which is an internal reference that may not be accessible to all readers of the code. This is fine as a development note, but could be confusing in a public codebase.
6 [NIT] llm/client.go 213 The anthropicRequest struct reformatting (removing alignment of struct fields) is a minor style inconsistency — the original had aligned columns and the new version doesn't. gofmt doesn't enforce column alignment so either is valid, but the change is cosmetically noisy in the diff for a struct that's otherwise unchanged.
7 [NIT] llm/aicore_test.go 168 The comment // Use a pointer to capture the server URL for use in the handler in TestAICoreClient_CompleteAnthropic is slightly misleading — baseURL is a string, not a pointer. The variable is captured by closure. The comment could simply say // baseURL is set after server creation; captured by closure in handlers.

Recommendation

APPROVE — Approve. The implementation is correct, well-structured, and follows Go idioms. The double getToken call pattern in CompleteAnthropic and CompleteOpenAI is redundant (though functionally correct due to caching), and the variable shadowing of url in getDeploymentURL is a minor nit. None of these issues affect correctness or safety. The test coverage is thorough and uses httptest correctly per conventions. CI passes.


Review by sonnet


Evaluated against 03a26a4f

# Sonnet Review ## Summary This PR adds a well-implemented native SAP AI Core provider with OAuth2 token caching, deployment discovery, and routing to both Anthropic and OpenAI endpoints. The code follows Go idioms, is well-tested with httptest, and CI passes. There are a few minor issues worth noting but nothing blocking. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 221 | In `CompleteAnthropic`, the token is fetched after `getDeploymentURL` which itself calls `getToken` internally. This results in two calls to `getToken` — one inside `getDeploymentURL` and one explicit call afterwards. The second call will typically be a cheap cache hit, but it's redundant and slightly misleading. Consider using the token returned from `getDeploymentURL` by refactoring, or at minimum add a comment explaining why the second fetch is safe/cheap. | | 2 | [MINOR] | `llm/aicore.go` | 280 | Same double `getToken` pattern in `CompleteOpenAI` — `getDeploymentURL` already fetches a token internally, then another explicit `getToken` call follows. Same concern as above. | | 3 | [MINOR] | `llm/aicore.go` | 190 | When multiple goroutines concurrently call `getDeploymentURL` for different models that are both missing from the cache, they will all serialize on the write lock and call `fetchDeployments` once (due to the double-check). However, if the first `fetchDeployments` populates only some models and a concurrent caller for a different model races past the first read-lock check but before the write-lock check, behavior is correct. This is fine. But there's a subtle issue: the token fetched before acquiring the write lock in `getDeploymentURL` could expire by the time `fetchDeployments` uses it (especially with a short-lived token). Since the code holds `getToken(ctx)` result and passes it to `fetchDeployments`, but `fetchDeployments` is called potentially after a long wait for the write lock, the token might be expired. Given the 5-minute refresh buffer this is extremely unlikely in practice, but worth documenting. | | 4 | [MINOR] | `llm/aicore.go` | 194 | The `getDeploymentURL` function shadows the builtin `url` identifier from the `net/url` package imported at the top of the file. Since `net/url` is only used in `fetchToken` and not in `getDeploymentURL`, this shadowing doesn't cause a bug, but it is slightly surprising. Consider renaming the local variable to `deployURL` to be consistent with the naming used elsewhere and avoid the shadow. | | 5 | [NIT] | `llm/aicore.go` | 47 | The `AICoreClient` struct doc comment references 'issue #54 review discussion' which is an internal reference that may not be accessible to all readers of the code. This is fine as a development note, but could be confusing in a public codebase. | | 6 | [NIT] | `llm/client.go` | 213 | The `anthropicRequest` struct reformatting (removing alignment of struct fields) is a minor style inconsistency — the original had aligned columns and the new version doesn't. `gofmt` doesn't enforce column alignment so either is valid, but the change is cosmetically noisy in the diff for a struct that's otherwise unchanged. | | 7 | [NIT] | `llm/aicore_test.go` | 168 | The comment `// Use a pointer to capture the server URL for use in the handler` in `TestAICoreClient_CompleteAnthropic` is slightly misleading — `baseURL` is a `string`, not a pointer. The variable is captured by closure. The comment could simply say `// baseURL is set after server creation; captured by closure in handlers.` | ## Recommendation **APPROVE** — Approve. The implementation is correct, well-structured, and follows Go idioms. The double `getToken` call pattern in `CompleteAnthropic` and `CompleteOpenAI` is redundant (though functionally correct due to caching), and the variable shadowing of `url` in `getDeploymentURL` is a minor nit. None of these issues affect correctness or safety. The test coverage is thorough and uses `httptest` correctly per conventions. CI passes. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 03a26a4f*
llm/aicore.go Outdated
@@ -0,0 +44,4 @@
token string
tokenExpiry time.Time
deployments map[string]string // model name -> deployment URL
}
First-time contributor

[NIT] The AICoreClient struct doc comment references 'issue #54 review discussion' which is an internal reference that may not be accessible to all readers of the code. This is fine as a development note, but could be confusing in a public codebase.

**[NIT]** The `AICoreClient` struct doc comment references 'issue #54 review discussion' which is an internal reference that may not be accessible to all readers of the code. This is fine as a development note, but could be confusing in a public codebase.
llm/aicore.go Outdated
@@ -0,0 +187,4 @@
req.Header.Set("AI-Resource-Group", c.config.ResourceGroup)
resp, err := c.http.Do(req)
if err != nil {
First-time contributor

[MINOR] When multiple goroutines concurrently call getDeploymentURL for different models that are both missing from the cache, they will all serialize on the write lock and call fetchDeployments once (due to the double-check). However, if the first fetchDeployments populates only some models and a concurrent caller for a different model races past the first read-lock check but before the write-lock check, behavior is correct. This is fine. But there's a subtle issue: the token fetched before acquiring the write lock in getDeploymentURL could expire by the time fetchDeployments uses it (especially with a short-lived token). Since the code holds getToken(ctx) result and passes it to fetchDeployments, but fetchDeployments is called potentially after a long wait for the write lock, the token might be expired. Given the 5-minute refresh buffer this is extremely unlikely in practice, but worth documenting.

**[MINOR]** When multiple goroutines concurrently call `getDeploymentURL` for different models that are both missing from the cache, they will all serialize on the write lock and call `fetchDeployments` once (due to the double-check). However, if the first `fetchDeployments` populates only some models and a concurrent caller for a different model races past the first read-lock check but before the write-lock check, behavior is correct. This is fine. But there's a subtle issue: the token fetched before acquiring the write lock in `getDeploymentURL` could expire by the time `fetchDeployments` uses it (especially with a short-lived token). Since the code holds `getToken(ctx)` result and passes it to `fetchDeployments`, but `fetchDeployments` is called potentially after a long wait for the write lock, the token might be expired. Given the 5-minute refresh buffer this is extremely unlikely in practice, but worth documenting.
llm/aicore.go Outdated
@@ -0,0 +191,4 @@
return fmt.Errorf("deployments request: %w", err)
}
defer resp.Body.Close()
First-time contributor

[MINOR] The getDeploymentURL function shadows the builtin url identifier from the net/url package imported at the top of the file. Since net/url is only used in fetchToken and not in getDeploymentURL, this shadowing doesn't cause a bug, but it is slightly surprising. Consider renaming the local variable to deployURL to be consistent with the naming used elsewhere and avoid the shadow.

**[MINOR]** The `getDeploymentURL` function shadows the builtin `url` identifier from the `net/url` package imported at the top of the file. Since `net/url` is only used in `fetchToken` and not in `getDeploymentURL`, this shadowing doesn't cause a bug, but it is slightly surprising. Consider renaming the local variable to `deployURL` to be consistent with the naming used elsewhere and avoid the shadow.
llm/aicore.go Outdated
@@ -0,0 +218,4 @@
}
if err := json.Unmarshal(body, &deployResp); err != nil {
return fmt.Errorf("parse deployments response: %w", err)
}
First-time contributor

[MINOR] In CompleteAnthropic, the token is fetched after getDeploymentURL which itself calls getToken internally. This results in two calls to getToken — one inside getDeploymentURL and one explicit call afterwards. The second call will typically be a cheap cache hit, but it's redundant and slightly misleading. Consider using the token returned from getDeploymentURL by refactoring, or at minimum add a comment explaining why the second fetch is safe/cheap.

**[MINOR]** In `CompleteAnthropic`, the token is fetched after `getDeploymentURL` which itself calls `getToken` internally. This results in two calls to `getToken` — one inside `getDeploymentURL` and one explicit call afterwards. The second call will typically be a cheap cache hit, but it's redundant and slightly misleading. Consider using the token returned from `getDeploymentURL` by refactoring, or at minimum add a comment explaining why the second fetch is safe/cheap.
llm/aicore.go Outdated
@@ -0,0 +277,4 @@
}
// AI Core uses /invoke for Anthropic models
invokeURL := strings.TrimRight(deployURL, "/") + "/invoke"
First-time contributor

[MINOR] Same double getToken pattern in CompleteOpenAIgetDeploymentURL already fetches a token internally, then another explicit getToken call follows. Same concern as above.

**[MINOR]** Same double `getToken` pattern in `CompleteOpenAI` — `getDeploymentURL` already fetches a token internally, then another explicit `getToken` call follows. Same concern as above.
@@ -0,0 +165,4 @@
t.Error("expected error for unknown model")
}
}
First-time contributor

[NIT] The comment // Use a pointer to capture the server URL for use in the handler in TestAICoreClient_CompleteAnthropic is slightly misleading — baseURL is a string, not a pointer. The variable is captured by closure. The comment could simply say // baseURL is set after server creation; captured by closure in handlers.

**[NIT]** The comment `// Use a pointer to capture the server URL for use in the handler` in `TestAICoreClient_CompleteAnthropic` is slightly misleading — `baseURL` is a `string`, not a pointer. The variable is captured by closure. The comment could simply say `// baseURL is set after server creation; captured by closure in handlers.`
llm/client.go Outdated
@@ -184,0 +210,4 @@
Model string `json:"model,omitempty"`
MaxTokens int `json:"max_tokens"`
System string `json:"system,omitempty"`
Messages []anthropicMsg `json:"messages"`
First-time contributor

[NIT] The anthropicRequest struct reformatting (removing alignment of struct fields) is a minor style inconsistency — the original had aligned columns and the new version doesn't. gofmt doesn't enforce column alignment so either is valid, but the change is cosmetically noisy in the diff for a struct that's otherwise unchanged.

**[NIT]** The `anthropicRequest` struct reformatting (removing alignment of struct fields) is a minor style inconsistency — the original had aligned columns and the new version doesn't. `gofmt` doesn't enforce column alignment so either is valid, but the change is cosmetically noisy in the diff for a struct that's otherwise unchanged.
gpt-review-bot approved these changes 2026-05-10 15:31:49 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Gpt Review

Summary

Well-scoped addition of native SAP AI Core support with clear CLI/action integration, robust token/deployment management, and comprehensive unit tests. Implementation is idiomatic, thread-safe with proper locking, error handling, and context propagation. CI passed.

Recommendation

APPROVE — The AI Core client design (OAuth token refresh, deployment discovery, Bedrock-format Anthropic, OpenAI chat API versioning) integrates cleanly with the existing llm.Client and CLI. Validation logic correctly differentiates provider requirements, and the composite action/CI workflow changes align with the new provider. Tests cover token caching/expiry, deployment selection, and both Anthropic/OpenAI invocation paths. Consider future enhancements such as an optional TTL for the deployment cache in long-lived processes and making the OpenAI API version configurable via flag/env. Otherwise, this is ready to merge.


Review by gpt


Evaluated against 03a26a4f

# Gpt Review ## Summary Well-scoped addition of native SAP AI Core support with clear CLI/action integration, robust token/deployment management, and comprehensive unit tests. Implementation is idiomatic, thread-safe with proper locking, error handling, and context propagation. CI passed. ## Recommendation **APPROVE** — The AI Core client design (OAuth token refresh, deployment discovery, Bedrock-format Anthropic, OpenAI chat API versioning) integrates cleanly with the existing llm.Client and CLI. Validation logic correctly differentiates provider requirements, and the composite action/CI workflow changes align with the new provider. Tests cover token caching/expiry, deployment selection, and both Anthropic/OpenAI invocation paths. Consider future enhancements such as an optional TTL for the deployment cache in long-lived processes and making the OpenAI API version configurable via flag/env. Otherwise, this is ready to merge. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 03a26a4f*
security-review-bot requested review from security-review-bot 2026-05-10 15:32:35 +00:00
security-review-bot approved these changes 2026-05-10 15:32:36 +00:00
Dismissed
security-review-bot left a comment
Collaborator

Security Review

Summary

The addition of native SAP AI Core support is well-structured, uses OAuth2 correctly, caches tokens safely, limits error-body leakage, and integrates cleanly with the CLI and CI. CI has passed and I found no exploitable vulnerabilities; a small hardening improvement is recommended.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 103 AuthURL is taken from configuration without scheme validation. If misconfigured to use plain http or a non-HTTPS URL, client credentials could be sent in cleartext and requests could be directed to untrusted endpoints. Consider enforcing or validating an https scheme (or at least logging/warning on non-HTTPS) before issuing the token request.
2 [MINOR] llm/aicore.go 181 APIURL is used to construct the deployments endpoint without scheme validation. To reduce the risk of accidental plaintext transmission or SSRF-like misuse in misconfigured environments, consider enforcing or validating an https scheme for APIURL before making requests.

Recommendation

APPROVE — Overall, the implementation is secure and idiomatic: OAuth client-credentials flow is used properly, tokens are cached with a refresh window under a mutex, Anthropic and OpenAI paths for AI Core are separated, sensitive response bodies are truncated in errors, and integration points in the CLI and CI avoid logging secrets. As a defense-in-depth improvement, add validation that AICore AuthURL and APIURL use HTTPS (reject or warn on http), to prevent accidental plaintext credential leakage and reduce the risk of directing requests to unintended hosts in misconfigured setups. With this minor hardening, the change is ready to merge.


Review by security


Evaluated against 03a26a4f

# Security Review ## Summary The addition of native SAP AI Core support is well-structured, uses OAuth2 correctly, caches tokens safely, limits error-body leakage, and integrates cleanly with the CLI and CI. CI has passed and I found no exploitable vulnerabilities; a small hardening improvement is recommended. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 103 | AuthURL is taken from configuration without scheme validation. If misconfigured to use plain http or a non-HTTPS URL, client credentials could be sent in cleartext and requests could be directed to untrusted endpoints. Consider enforcing or validating an https scheme (or at least logging/warning on non-HTTPS) before issuing the token request. | | 2 | [MINOR] | `llm/aicore.go` | 181 | APIURL is used to construct the deployments endpoint without scheme validation. To reduce the risk of accidental plaintext transmission or SSRF-like misuse in misconfigured environments, consider enforcing or validating an https scheme for APIURL before making requests. | ## Recommendation **APPROVE** — Overall, the implementation is secure and idiomatic: OAuth client-credentials flow is used properly, tokens are cached with a refresh window under a mutex, Anthropic and OpenAI paths for AI Core are separated, sensitive response bodies are truncated in errors, and integration points in the CLI and CI avoid logging secrets. As a defense-in-depth improvement, add validation that AICore AuthURL and APIURL use HTTPS (reject or warn on http), to prevent accidental plaintext credential leakage and reduce the risk of directing requests to unintended hosts in misconfigured setups. With this minor hardening, the change is ready to merge. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against 03a26a4f*
llm/aicore.go Outdated
@@ -0,0 +100,4 @@
}
func (c *AICoreClient) fetchToken(ctx context.Context) (string, time.Time, error) {
tokenURL := strings.TrimRight(c.config.AuthURL, "/") + "/oauth/token"
Collaborator

[MINOR] AuthURL is taken from configuration without scheme validation. If misconfigured to use plain http or a non-HTTPS URL, client credentials could be sent in cleartext and requests could be directed to untrusted endpoints. Consider enforcing or validating an https scheme (or at least logging/warning on non-HTTPS) before issuing the token request.

**[MINOR]** AuthURL is taken from configuration without scheme validation. If misconfigured to use plain http or a non-HTTPS URL, client credentials could be sent in cleartext and requests could be directed to untrusted endpoints. Consider enforcing or validating an https scheme (or at least logging/warning on non-HTTPS) before issuing the token request.
llm/aicore.go Outdated
@@ -0,0 +178,4 @@
}
func (c *AICoreClient) fetchDeployments(ctx context.Context, token string) error {
deployURL := strings.TrimRight(c.config.APIURL, "/") + "/v2/lm/deployments"
Collaborator

[MINOR] APIURL is used to construct the deployments endpoint without scheme validation. To reduce the risk of accidental plaintext transmission or SSRF-like misuse in misconfigured environments, consider enforcing or validating an https scheme for APIURL before making requests.

**[MINOR]** APIURL is used to construct the deployments endpoint without scheme validation. To reduce the risk of accidental plaintext transmission or SSRF-like misuse in misconfigured environments, consider enforcing or validating an https scheme for APIURL before making requests.
rodin force-pushed feat/aicore-provider-v2 from 03a26a4f8c to f364930bbd 2026-05-10 15:35:57 +00:00 Compare
sonnet-review-bot approved these changes 2026-05-10 15:36:45 +00:00
sonnet-review-bot left a comment
First-time contributor

Sonnet Review

Summary

This PR adds a well-structured native SAP AI Core provider with OAuth2 token refresh, deployment discovery, and proper routing between Anthropic and OpenAI models. The implementation is clean, thread-safe, follows established patterns in the codebase, and is thoroughly tested. CI passes.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 163 Duplicate doc comment: the getDeploymentURL function has its doc comment written twice (lines 163-172 and 173-179 in the file). The first occurrence should be removed.
2 [MINOR] llm/aicore.go 267 The maxTokens parameter on CompleteAnthropic is always called with the hardcoded value 8192 (both from completeAICore in client.go and from the test). Consider either removing the parameter and hardcoding it internally, or making it a constant to avoid the magic number at call sites.
3 [NIT] llm/aicore.go 97 The token refresh buffer of 5 minutes (time.Now().Add(5*time.Minute).Before(c.tokenExpiry)) is hardcoded in two places in getToken. This could be extracted as a named constant (e.g. tokenRefreshBuffer = 5 * time.Minute) to make the intent clearer and avoid duplication.
4 [NIT] llm/aicore.go 188 In getDeploymentURL, when the deployment is found in cache on the read-lock path, getToken is called after releasing the read lock. This is correct but means the token could have been refreshed unnecessarily. This is a minor inefficiency acceptable for CI runners but worth noting in longer-lived use.
5 [NIT] llm/aicore_test.go 29 The test TestAICoreClient_TokenFetch uses json.NewEncoder(w).Encode(...) without checking the error. While errors on httptest.ResponseRecorder writes are extremely rare, for consistency with the Go convention of always checking errors, consider assigning and ignoring explicitly or using must helpers.

Recommendation

APPROVE — Approve. The implementation is solid: correct double-checked locking pattern for thread safety, proper token refresh with expiry buffer, deployment caching with appropriate lifetime documentation, model-type routing via the anthropic-- prefix convention, and comprehensive unit tests covering all major paths. The two MINOR findings are quality-of-life improvements (duplicate comment cleanup, magic number extraction) rather than bugs. The CI passes.


Review by sonnet


Evaluated against f364930b

# Sonnet Review ## Summary This PR adds a well-structured native SAP AI Core provider with OAuth2 token refresh, deployment discovery, and proper routing between Anthropic and OpenAI models. The implementation is clean, thread-safe, follows established patterns in the codebase, and is thoroughly tested. CI passes. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 163 | Duplicate doc comment: the `getDeploymentURL` function has its doc comment written twice (lines 163-172 and 173-179 in the file). The first occurrence should be removed. | | 2 | [MINOR] | `llm/aicore.go` | 267 | The `maxTokens` parameter on `CompleteAnthropic` is always called with the hardcoded value 8192 (both from `completeAICore` in client.go and from the test). Consider either removing the parameter and hardcoding it internally, or making it a constant to avoid the magic number at call sites. | | 3 | [NIT] | `llm/aicore.go` | 97 | The token refresh buffer of 5 minutes (`time.Now().Add(5*time.Minute).Before(c.tokenExpiry)`) is hardcoded in two places in `getToken`. This could be extracted as a named constant (e.g. `tokenRefreshBuffer = 5 * time.Minute`) to make the intent clearer and avoid duplication. | | 4 | [NIT] | `llm/aicore.go` | 188 | In `getDeploymentURL`, when the deployment is found in cache on the read-lock path, `getToken` is called after releasing the read lock. This is correct but means the token could have been refreshed unnecessarily. This is a minor inefficiency acceptable for CI runners but worth noting in longer-lived use. | | 5 | [NIT] | `llm/aicore_test.go` | 29 | The test `TestAICoreClient_TokenFetch` uses `json.NewEncoder(w).Encode(...)` without checking the error. While errors on `httptest.ResponseRecorder` writes are extremely rare, for consistency with the Go convention of always checking errors, consider assigning and ignoring explicitly or using `must` helpers. | ## Recommendation **APPROVE** — Approve. The implementation is solid: correct double-checked locking pattern for thread safety, proper token refresh with expiry buffer, deployment caching with appropriate lifetime documentation, model-type routing via the `anthropic--` prefix convention, and comprehensive unit tests covering all major paths. The two MINOR findings are quality-of-life improvements (duplicate comment cleanup, magic number extraction) rather than bugs. The CI passes. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against f364930b*
llm/aicore.go Outdated
@@ -0,0 +94,4 @@
if err != nil {
return "", err
}
c.token = token
First-time contributor

[NIT] The token refresh buffer of 5 minutes (time.Now().Add(5*time.Minute).Before(c.tokenExpiry)) is hardcoded in two places in getToken. This could be extracted as a named constant (e.g. tokenRefreshBuffer = 5 * time.Minute) to make the intent clearer and avoid duplication.

**[NIT]** The token refresh buffer of 5 minutes (`time.Now().Add(5*time.Minute).Before(c.tokenExpiry)`) is hardcoded in two places in `getToken`. This could be extracted as a named constant (e.g. `tokenRefreshBuffer = 5 * time.Minute`) to make the intent clearer and avoid duplication.
llm/aicore.go Outdated
@@ -0,0 +160,4 @@
token, err = c.getToken(ctx)
if err != nil {
return "", "", fmt.Errorf("get token: %w", err)
}
First-time contributor

[MINOR] Duplicate doc comment: the getDeploymentURL function has its doc comment written twice (lines 163-172 and 173-179 in the file). The first occurrence should be removed.

**[MINOR]** Duplicate doc comment: the `getDeploymentURL` function has its doc comment written twice (lines 163-172 and 173-179 in the file). The first occurrence should be removed.
llm/aicore.go Outdated
@@ -0,0 +185,4 @@
if u, ok := c.deployments[model]; ok {
return u, token, nil
}
First-time contributor

[NIT] In getDeploymentURL, when the deployment is found in cache on the read-lock path, getToken is called after releasing the read lock. This is correct but means the token could have been refreshed unnecessarily. This is a minor inefficiency acceptable for CI runners but worth noting in longer-lived use.

**[NIT]** In `getDeploymentURL`, when the deployment is found in cache on the read-lock path, `getToken` is called after releasing the read lock. This is correct but means the token could have been refreshed unnecessarily. This is a minor inefficiency acceptable for CI runners but worth noting in longer-lived use.
llm/aicore.go Outdated
@@ -0,0 +264,4 @@
Role: m.Role,
Content: m.Content,
})
}
First-time contributor

[MINOR] The maxTokens parameter on CompleteAnthropic is always called with the hardcoded value 8192 (both from completeAICore in client.go and from the test). Consider either removing the parameter and hardcoding it internally, or making it a constant to avoid the magic number at call sites.

**[MINOR]** The `maxTokens` parameter on `CompleteAnthropic` is always called with the hardcoded value 8192 (both from `completeAICore` in client.go and from the test). Consider either removing the parameter and hardcoding it internally, or making it a constant to avoid the magic number at call sites.
@@ -0,0 +26,4 @@
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"access_token": "test-token-123",
"expires_in": 3600,
First-time contributor

[NIT] The test TestAICoreClient_TokenFetch uses json.NewEncoder(w).Encode(...) without checking the error. While errors on httptest.ResponseRecorder writes are extremely rare, for consistency with the Go convention of always checking errors, consider assigning and ignoring explicitly or using must helpers.

**[NIT]** The test `TestAICoreClient_TokenFetch` uses `json.NewEncoder(w).Encode(...)` without checking the error. While errors on `httptest.ResponseRecorder` writes are extremely rare, for consistency with the Go convention of always checking errors, consider assigning and ignoring explicitly or using `must` helpers.
security-review-bot requested review from security-review-bot 2026-05-10 15:37:27 +00:00
security-review-bot approved these changes 2026-05-10 15:37:27 +00:00
Dismissed
security-review-bot left a comment
Collaborator

Security Review

Summary

Security-focused review finds the new SAP AI Core integration well-structured with token caching, bounded error logging, and environment-based configuration. No exploitable vulnerabilities were identified; a couple of defense-in-depth improvements are recommended.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 1 AI Core endpoints (AuthURL/APIURL) are used as provided without enforcing HTTPS. Consider validating the URL scheme to be https before sending client credentials or tokens to prevent accidental plaintext transmission due to misconfiguration.
2 [MINOR] llm/aicore.go 200 The deployment URL returned by the AI Core API is used verbatim for follow-up requests with the bearer token. As a defense-in-depth measure, consider verifying that the deployment URL host matches or is within the expected API domain to reduce the risk of token exfiltration if the deployments listing is compromised.

Recommendation

APPROVE — Overall, the additions for native SAP AI Core support are secure and align with best practices: OAuth2 tokens are cached, error bodies are truncated to prevent excessive leakage, timeouts are set, and symlink/path traversal protections remain in place for system prompts. To further harden the implementation, validate that AICore AuthURL/APIURL use HTTPS to avoid accidental plaintext credential transmission, and optionally restrict deployment URLs to the expected domain before sending Authorization headers. With these minor defense-in-depth improvements considered, the PR is approved.


Review by security


Evaluated against f364930b

# Security Review ## Summary Security-focused review finds the new SAP AI Core integration well-structured with token caching, bounded error logging, and environment-based configuration. No exploitable vulnerabilities were identified; a couple of defense-in-depth improvements are recommended. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 1 | AI Core endpoints (AuthURL/APIURL) are used as provided without enforcing HTTPS. Consider validating the URL scheme to be https before sending client credentials or tokens to prevent accidental plaintext transmission due to misconfiguration. | | 2 | [MINOR] | `llm/aicore.go` | 200 | The deployment URL returned by the AI Core API is used verbatim for follow-up requests with the bearer token. As a defense-in-depth measure, consider verifying that the deployment URL host matches or is within the expected API domain to reduce the risk of token exfiltration if the deployments listing is compromised. | ## Recommendation **APPROVE** — Overall, the additions for native SAP AI Core support are secure and align with best practices: OAuth2 tokens are cached, error bodies are truncated to prevent excessive leakage, timeouts are set, and symlink/path traversal protections remain in place for system prompts. To further harden the implementation, validate that AICore AuthURL/APIURL use HTTPS to avoid accidental plaintext credential transmission, and optionally restrict deployment URLs to the expected domain before sending Authorization headers. With these minor defense-in-depth improvements considered, the PR is approved. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against f364930b*
llm/aicore.go Outdated
@@ -0,0 +1,391 @@
package llm
Collaborator

[MINOR] AI Core endpoints (AuthURL/APIURL) are used as provided without enforcing HTTPS. Consider validating the URL scheme to be https before sending client credentials or tokens to prevent accidental plaintext transmission due to misconfiguration.

**[MINOR]** AI Core endpoints (AuthURL/APIURL) are used as provided without enforcing HTTPS. Consider validating the URL scheme to be https before sending client credentials or tokens to prevent accidental plaintext transmission due to misconfiguration.
llm/aicore.go Outdated
@@ -0,0 +197,4 @@
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("AI-Resource-Group", c.config.ResourceGroup)
Collaborator

[MINOR] The deployment URL returned by the AI Core API is used verbatim for follow-up requests with the bearer token. As a defense-in-depth measure, consider verifying that the deployment URL host matches or is within the expected API domain to reduce the risk of token exfiltration if the deployments listing is compromised.

**[MINOR]** The deployment URL returned by the AI Core API is used verbatim for follow-up requests with the bearer token. As a defense-in-depth measure, consider verifying that the deployment URL host matches or is within the expected API domain to reduce the risk of token exfiltration if the deployments listing is compromised.
gpt-review-bot approved these changes 2026-05-10 15:38:13 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Gpt Review

Summary

The PR cleanly introduces native SAP AI Core support with a dedicated client, integrates it into the CLI and CI, and extends the composite action and README accordingly. The implementation is thread-safe, handles OAuth token refresh, deployment discovery, and routes requests appropriately for Anthropic and OpenAI models, with solid test coverage. Only minor nits were found.

Findings

# Severity File Line Finding
1 [NIT] llm/aicore.go 170 Duplicate comment line above getDeploymentURL: the line 'getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.' appears twice and can be deduplicated.
2 [MINOR] llm/aicore.go 302 In CompleteOpenAI, the request body includes the 'model' field, but AI Core deployments already specify the model. This is usually harmless, but consider confirming it cannot conflict with deployment settings and documenting the intent (unlike the Anthropic path where model is omitted).

Recommendation

APPROVE — Overall, the changes are well-structured, idiomatic, and aligned with repository conventions. The AI Core client uses proper concurrency controls for token and deployment caching, limits error body exposure, and includes comprehensive tests that exercise token refresh, deployment selection, and both Anthropic and OpenAI invocation paths. The CLI validation and CI workflow updates are consistent with the new provider. Address the small nit about the duplicated comment in aicore.go and optionally clarify the rationale for including the model field in OpenAI requests despite deployments defining it. With these minor touch-ups, this is ready to merge.


Review by gpt


Evaluated against f364930b

# Gpt Review ## Summary The PR cleanly introduces native SAP AI Core support with a dedicated client, integrates it into the CLI and CI, and extends the composite action and README accordingly. The implementation is thread-safe, handles OAuth token refresh, deployment discovery, and routes requests appropriately for Anthropic and OpenAI models, with solid test coverage. Only minor nits were found. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [NIT] | `llm/aicore.go` | 170 | Duplicate comment line above getDeploymentURL: the line 'getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.' appears twice and can be deduplicated. | | 2 | [MINOR] | `llm/aicore.go` | 302 | In CompleteOpenAI, the request body includes the 'model' field, but AI Core deployments already specify the model. This is usually harmless, but consider confirming it cannot conflict with deployment settings and documenting the intent (unlike the Anthropic path where model is omitted). | ## Recommendation **APPROVE** — Overall, the changes are well-structured, idiomatic, and aligned with repository conventions. The AI Core client uses proper concurrency controls for token and deployment caching, limits error body exposure, and includes comprehensive tests that exercise token refresh, deployment selection, and both Anthropic and OpenAI invocation paths. The CLI validation and CI workflow updates are consistent with the new provider. Address the small nit about the duplicated comment in aicore.go and optionally clarify the rationale for including the model field in OpenAI requests despite deployments defining it. With these minor touch-ups, this is ready to merge. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against f364930b*
llm/aicore.go Outdated
@@ -0,0 +167,4 @@
// Fetch token first (before acquiring write lock to avoid holding lock during I/O)
token, err = c.getToken(ctx)
if err != nil {
First-time contributor

[NIT] Duplicate comment line above getDeploymentURL: the line 'getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.' appears twice and can be deduplicated.

**[NIT]** Duplicate comment line above getDeploymentURL: the line 'getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.' appears twice and can be deduplicated.
llm/aicore.go Outdated
@@ -0,0 +299,4 @@
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
First-time contributor

[MINOR] In CompleteOpenAI, the request body includes the 'model' field, but AI Core deployments already specify the model. This is usually harmless, but consider confirming it cannot conflict with deployment settings and documenting the intent (unlike the Anthropic path where model is omitted).

**[MINOR]** In CompleteOpenAI, the request body includes the 'model' field, but AI Core deployments already specify the model. This is usually harmless, but consider confirming it cannot conflict with deployment settings and documenting the intent (unlike the Anthropic path where model is omitted).
rodin force-pushed feat/aicore-provider-v2 from f364930bbd to 64ffb8fb90 2026-05-10 15:38:45 +00:00 Compare
rodin force-pushed feat/aicore-provider-v2 from 64ffb8fb90 to 6c2b36e29d 2026-05-10 15:39:06 +00:00 Compare
sonnet-review-bot approved these changes 2026-05-10 15:39:35 +00:00
sonnet-review-bot left a comment
First-time contributor

Sonnet Review

Summary

This PR adds a well-implemented SAP AI Core provider with OAuth2 token management, deployment discovery, and routing to Anthropic/OpenAI endpoints. The code is idiomatic Go, thread-safe with proper double-checked locking, error handling is consistent, and test coverage is comprehensive with httptest-based integration tests.

Findings

# Severity File Line Finding
1 [NIT] llm/aicore.go 155 Duplicate doc comment: // getDeploymentURL returns the deployment URL for a model, fetching deployments if needed. appears twice consecutively (lines ~154-155 and ~156-160 in the source). One copy should be removed.
2 [NIT] llm/aicore.go 283 The hardcoded string "bedrock-2023-05-31" in CompleteAnthropic could be a named constant like AICoreAnthropicVersion similar to AICoreOpenAIAPIVersion, making it easier to update and self-documenting.
3 [NIT] llm/aicore.go 57 The field name http shadows the standard library package net/http. While this works because the package is only referenced by its qualified name within the struct methods, renaming to httpClient would avoid the potential confusion and better align with the pattern used elsewhere (e.g. c.http.Do).
4 [NIT] llm/aicore_test.go 29 Test uses json.NewEncoder(w).Encode(...) without checking the error return. While test code is less critical, this pattern appears throughout the test file. A helper like mustEncodeJSON(t, w, v) would be cleaner and consistent.
5 [MINOR] llm/aicore.go 198 When multiple models map to the same name (e.g. two RUNNING gpt-5 deployments in TestAICoreClient_DeploymentFetch), fetchDeployments silently overwrites with the last one seen. The test verifies it picks deploy-789 over deploy-456 (stopped), but if two RUNNING deployments existed for the same model the behavior is undefined (last-write-wins from iteration order). A log warning or first-wins strategy would be more predictable, though this is unlikely in practice.

Recommendation

APPROVE — Approve. The implementation is correct, well-tested, idiomatic Go, follows the repository conventions (stdlib only, errors returned not panicked, context propagation, httptest for mocking), and the concurrency model (double-checked locking with RWMutex) is appropriate. The findings are all minor/nit-level and don't affect correctness. The duplicate doc comment (NIT) is the only thing worth fixing before merge but doesn't warrant blocking.


Review by sonnet


Evaluated against 6c2b36e2

# Sonnet Review ## Summary This PR adds a well-implemented SAP AI Core provider with OAuth2 token management, deployment discovery, and routing to Anthropic/OpenAI endpoints. The code is idiomatic Go, thread-safe with proper double-checked locking, error handling is consistent, and test coverage is comprehensive with httptest-based integration tests. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [NIT] | `llm/aicore.go` | 155 | Duplicate doc comment: `// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.` appears twice consecutively (lines ~154-155 and ~156-160 in the source). One copy should be removed. | | 2 | [NIT] | `llm/aicore.go` | 283 | The hardcoded string `"bedrock-2023-05-31"` in `CompleteAnthropic` could be a named constant like `AICoreAnthropicVersion` similar to `AICoreOpenAIAPIVersion`, making it easier to update and self-documenting. | | 3 | [NIT] | `llm/aicore.go` | 57 | The field name `http` shadows the standard library package `net/http`. While this works because the package is only referenced by its qualified name within the struct methods, renaming to `httpClient` would avoid the potential confusion and better align with the pattern used elsewhere (e.g. `c.http.Do`). | | 4 | [NIT] | `llm/aicore_test.go` | 29 | Test uses `json.NewEncoder(w).Encode(...)` without checking the error return. While test code is less critical, this pattern appears throughout the test file. A helper like `mustEncodeJSON(t, w, v)` would be cleaner and consistent. | | 5 | [MINOR] | `llm/aicore.go` | 198 | When multiple models map to the same name (e.g. two RUNNING gpt-5 deployments in `TestAICoreClient_DeploymentFetch`), `fetchDeployments` silently overwrites with the last one seen. The test verifies it picks `deploy-789` over `deploy-456` (stopped), but if two RUNNING deployments existed for the same model the behavior is undefined (last-write-wins from iteration order). A log warning or first-wins strategy would be more predictable, though this is unlikely in practice. | ## Recommendation **APPROVE** — Approve. The implementation is correct, well-tested, idiomatic Go, follows the repository conventions (stdlib only, errors returned not panicked, context propagation, httptest for mocking), and the concurrency model (double-checked locking with RWMutex) is appropriate. The findings are all minor/nit-level and don't affect correctness. The duplicate doc comment (NIT) is the only thing worth fixing before merge but doesn't warrant blocking. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 6c2b36e2*
llm/aicore.go Outdated
@@ -0,0 +54,4 @@
http: &http.Client{Timeout: 5 * time.Minute},
deployments: make(map[string]string),
}
}
First-time contributor

[NIT] The field name http shadows the standard library package net/http. While this works because the package is only referenced by its qualified name within the struct methods, renaming to httpClient would avoid the potential confusion and better align with the pattern used elsewhere (e.g. c.http.Do).

**[NIT]** The field name `http` shadows the standard library package `net/http`. While this works because the package is only referenced by its qualified name within the struct methods, renaming to `httpClient` would avoid the potential confusion and better align with the pattern used elsewhere (e.g. `c.http.Do`).
llm/aicore.go Outdated
@@ -0,0 +152,4 @@
// during network I/O. In rare cases where multiple goroutines race and one waits a long
// time for the write lock, the token could theoretically expire. The 5-minute refresh
// buffer in getToken makes this extremely unlikely in practice.
func (c *AICoreClient) getDeploymentURL(ctx context.Context, model string) (deployURL, token string, err error) {
First-time contributor

[NIT] Duplicate doc comment: // getDeploymentURL returns the deployment URL for a model, fetching deployments if needed. appears twice consecutively (lines ~154-155 and ~156-160 in the source). One copy should be removed.

**[NIT]** Duplicate doc comment: `// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.` appears twice consecutively (lines ~154-155 and ~156-160 in the source). One copy should be removed.
llm/aicore.go Outdated
@@ -0,0 +195,4 @@
if err != nil {
return fmt.Errorf("create deployments request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
First-time contributor

[MINOR] When multiple models map to the same name (e.g. two RUNNING gpt-5 deployments in TestAICoreClient_DeploymentFetch), fetchDeployments silently overwrites with the last one seen. The test verifies it picks deploy-789 over deploy-456 (stopped), but if two RUNNING deployments existed for the same model the behavior is undefined (last-write-wins from iteration order). A log warning or first-wins strategy would be more predictable, though this is unlikely in practice.

**[MINOR]** When multiple models map to the same name (e.g. two RUNNING gpt-5 deployments in `TestAICoreClient_DeploymentFetch`), `fetchDeployments` silently overwrites with the last one seen. The test verifies it picks `deploy-789` over `deploy-456` (stopped), but if two RUNNING deployments existed for the same model the behavior is undefined (last-write-wins from iteration order). A log warning or first-wins strategy would be more predictable, though this is unlikely in practice.
llm/aicore.go Outdated
@@ -0,0 +280,4 @@
data, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("marshal request: %w", err)
First-time contributor

[NIT] The hardcoded string "bedrock-2023-05-31" in CompleteAnthropic could be a named constant like AICoreAnthropicVersion similar to AICoreOpenAIAPIVersion, making it easier to update and self-documenting.

**[NIT]** The hardcoded string `"bedrock-2023-05-31"` in `CompleteAnthropic` could be a named constant like `AICoreAnthropicVersion` similar to `AICoreOpenAIAPIVersion`, making it easier to update and self-documenting.
@@ -0,0 +26,4 @@
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"access_token": "test-token-123",
"expires_in": 3600,
First-time contributor

[NIT] Test uses json.NewEncoder(w).Encode(...) without checking the error return. While test code is less critical, this pattern appears throughout the test file. A helper like mustEncodeJSON(t, w, v) would be cleaner and consistent.

**[NIT]** Test uses `json.NewEncoder(w).Encode(...)` without checking the error return. While test code is less critical, this pattern appears throughout the test file. A helper like `mustEncodeJSON(t, w, v)` would be cleaner and consistent.
sonnet-review-bot approved these changes 2026-05-10 15:39:50 +00:00
sonnet-review-bot left a comment
First-time contributor

Sonnet Review

Summary

This PR adds a well-structured SAP AI Core provider implementation with proper OAuth2 token caching, deployment discovery, model routing (Anthropic vs OpenAI), and comprehensive unit tests. The code follows established patterns in the codebase, handles errors correctly, and is thread-safe. CI passed.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 144 Duplicate doc comment on getDeploymentURL: the function comment appears twice (lines 143-144 and 145-151). The first one-liner is a leftover from before the fuller comment was added.
2 [MINOR] llm/aicore.go 277 CompleteAnthropic has a hardcoded maxTokens parameter in its signature but callers always pass 8192. This leaks an implementation detail through the API; consider making it a constant or removing the parameter since it's always 8192.
3 [NIT] llm/aicore.go 52 The field name http shadows the standard library package name net/http. Within the AICoreClient methods, http.MethodPost etc. still work because the package is imported separately, but this naming can confuse readers. The parent Client struct in client.go uses the same pattern, so it's consistent — but worth noting.
4 [NIT] llm/aicore.go 93 The token refresh threshold time.Now().Add(5*time.Minute).Before(c.tokenExpiry) is somewhat inverted in readability. c.tokenExpiry.After(time.Now().Add(5*time.Minute)) expresses the same condition more naturally: "expiry is more than 5 minutes in the future".
5 [NIT] llm/client.go 213 The struct literal alignment for anthropicRequest fields is inconsistent in the diff — AnthropicVersion and Model are right-aligned differently from the remaining fields. A gofmt run would normalize this (though it may already be correct after formatting; hard to tell from the diff context).

Recommendation

APPROVE — Approve with minor observations. The implementation is correct, well-tested, and follows the project's conventions. The only substantive issues are a duplicate doc comment line on getDeploymentURL (cosmetic) and the arguably unnecessary maxTokens parameter on CompleteAnthropic that is always called with 8192. Neither is a blocker. The concurrency design (RWMutex with double-checked locking for both token and deployment cache) is sound and well-documented.


Review by sonnet


Evaluated against 6c2b36e2

# Sonnet Review ## Summary This PR adds a well-structured SAP AI Core provider implementation with proper OAuth2 token caching, deployment discovery, model routing (Anthropic vs OpenAI), and comprehensive unit tests. The code follows established patterns in the codebase, handles errors correctly, and is thread-safe. CI passed. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 144 | Duplicate doc comment on `getDeploymentURL`: the function comment appears twice (lines 143-144 and 145-151). The first one-liner is a leftover from before the fuller comment was added. | | 2 | [MINOR] | `llm/aicore.go` | 277 | `CompleteAnthropic` has a hardcoded `maxTokens` parameter in its signature but callers always pass `8192`. This leaks an implementation detail through the API; consider making it a constant or removing the parameter since it's always 8192. | | 3 | [NIT] | `llm/aicore.go` | 52 | The field name `http` shadows the standard library package name `net/http`. Within the `AICoreClient` methods, `http.MethodPost` etc. still work because the package is imported separately, but this naming can confuse readers. The parent `Client` struct in `client.go` uses the same pattern, so it's consistent — but worth noting. | | 4 | [NIT] | `llm/aicore.go` | 93 | The token refresh threshold `time.Now().Add(5*time.Minute).Before(c.tokenExpiry)` is somewhat inverted in readability. `c.tokenExpiry.After(time.Now().Add(5*time.Minute))` expresses the same condition more naturally: "expiry is more than 5 minutes in the future". | | 5 | [NIT] | `llm/client.go` | 213 | The struct literal alignment for `anthropicRequest` fields is inconsistent in the diff — `AnthropicVersion` and `Model` are right-aligned differently from the remaining fields. A `gofmt` run would normalize this (though it may already be correct after formatting; hard to tell from the diff context). | ## Recommendation **APPROVE** — Approve with minor observations. The implementation is correct, well-tested, and follows the project's conventions. The only substantive issues are a duplicate doc comment line on `getDeploymentURL` (cosmetic) and the arguably unnecessary `maxTokens` parameter on `CompleteAnthropic` that is always called with 8192. Neither is a blocker. The concurrency design (RWMutex with double-checked locking for both token and deployment cache) is sound and well-documented. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 6c2b36e2*
llm/aicore.go Outdated
@@ -0,0 +49,4 @@
// NewAICoreClient creates a new AI Core client with the given configuration.
// The client uses a default 5-minute timeout; use WithTimeout to customize.
func NewAICoreClient(cfg AICoreConfig) *AICoreClient {
return &AICoreClient{
First-time contributor

[NIT] The field name http shadows the standard library package name net/http. Within the AICoreClient methods, http.MethodPost etc. still work because the package is imported separately, but this naming can confuse readers. The parent Client struct in client.go uses the same pattern, so it's consistent — but worth noting.

**[NIT]** The field name `http` shadows the standard library package name `net/http`. Within the `AICoreClient` methods, `http.MethodPost` etc. still work because the package is imported separately, but this naming can confuse readers. The parent `Client` struct in `client.go` uses the same pattern, so it's consistent — but worth noting.
llm/aicore.go Outdated
@@ -0,0 +90,4 @@
return c.token, nil
}
token, expiry, err := c.fetchToken(ctx)
First-time contributor

[NIT] The token refresh threshold time.Now().Add(5*time.Minute).Before(c.tokenExpiry) is somewhat inverted in readability. c.tokenExpiry.After(time.Now().Add(5*time.Minute)) expresses the same condition more naturally: "expiry is more than 5 minutes in the future".

**[NIT]** The token refresh threshold `time.Now().Add(5*time.Minute).Before(c.tokenExpiry)` is somewhat inverted in readability. `c.tokenExpiry.After(time.Now().Add(5*time.Minute))` expresses the same condition more naturally: "expiry is more than 5 minutes in the future".
llm/aicore.go Outdated
@@ -0,0 +141,4 @@
}
expiry := time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
return tokenResp.AccessToken, expiry, nil
First-time contributor

[MINOR] Duplicate doc comment on getDeploymentURL: the function comment appears twice (lines 143-144 and 145-151). The first one-liner is a leftover from before the fuller comment was added.

**[MINOR]** Duplicate doc comment on `getDeploymentURL`: the function comment appears twice (lines 143-144 and 145-151). The first one-liner is a leftover from before the fuller comment was added.
llm/aicore.go Outdated
@@ -0,0 +274,4 @@
System: system,
Messages: userMessages,
}
if temperature > 0 {
First-time contributor

[MINOR] CompleteAnthropic has a hardcoded maxTokens parameter in its signature but callers always pass 8192. This leaks an implementation detail through the API; consider making it a constant or removing the parameter since it's always 8192.

**[MINOR]** `CompleteAnthropic` has a hardcoded `maxTokens` parameter in its signature but callers always pass `8192`. This leaks an implementation detail through the API; consider making it a constant or removing the parameter since it's always 8192.
llm/client.go Outdated
@@ -184,0 +210,4 @@
Model string `json:"model,omitempty"`
MaxTokens int `json:"max_tokens"`
System string `json:"system,omitempty"`
Messages []anthropicMsg `json:"messages"`
First-time contributor

[NIT] The struct literal alignment for anthropicRequest fields is inconsistent in the diff — AnthropicVersion and Model are right-aligned differently from the remaining fields. A gofmt run would normalize this (though it may already be correct after formatting; hard to tell from the diff context).

**[NIT]** The struct literal alignment for `anthropicRequest` fields is inconsistent in the diff — `AnthropicVersion` and `Model` are right-aligned differently from the remaining fields. A `gofmt` run would normalize this (though it may already be correct after formatting; hard to tell from the diff context).
rodin removed the self-reviewed label 2026-05-10 15:40:05 +00:00
aweiker was unassigned by rodin 2026-05-10 15:40:06 +00:00
rodin self-assigned this 2026-05-10 15:40:06 +00:00
gpt-review-bot approved these changes 2026-05-10 15:40:12 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Gpt Review

Summary

Solid addition of native SAP AI Core support with clear flag/env integration, a well-structured client, and comprehensive tests. No major issues found; minor refinements suggested.

Findings

# Severity File Line Finding
1 [NIT] llm/aicore.go 170 Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed."). Remove the repeated line to improve readability.
2 [MINOR] llm/aicore.go 273 Consider handling 401 Unauthorized responses by refreshing the OAuth token and retrying once. This would improve resilience if the token expires earlier than anticipated despite the 5-minute refresh buffer.
3 [MINOR] llm/aicore.go 345 In CompleteOpenAI, the request body includes the "model" field even though AI Core deployments already bind the model. Consider omitting Model to rely solely on the deployment configuration, reducing potential mismatch risk.

Recommendation

APPROVE — The PR cleanly integrates SAP AI Core as a provider, adds appropriate flags and validation in the CLI, updates the composite action and CI workflow, and provides thorough unit tests covering token handling, deployment discovery, and both Anthropic and OpenAI paths. The concurrency and error-handling in the AI Core client are sensible, with token caching and truncated error bodies. Approve as is. For follow-up improvements, remove the duplicated comment in aicore.go, consider refreshing the token on 401 responses from AI Core endpoints, and potentially omit the "model" field for OpenAI requests through AI Core deployments to avoid configuration drift.


Review by gpt


Evaluated against 6c2b36e2

# Gpt Review ## Summary Solid addition of native SAP AI Core support with clear flag/env integration, a well-structured client, and comprehensive tests. No major issues found; minor refinements suggested. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [NIT] | `llm/aicore.go` | 170 | Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed."). Remove the repeated line to improve readability. | | 2 | [MINOR] | `llm/aicore.go` | 273 | Consider handling 401 Unauthorized responses by refreshing the OAuth token and retrying once. This would improve resilience if the token expires earlier than anticipated despite the 5-minute refresh buffer. | | 3 | [MINOR] | `llm/aicore.go` | 345 | In CompleteOpenAI, the request body includes the "model" field even though AI Core deployments already bind the model. Consider omitting Model to rely solely on the deployment configuration, reducing potential mismatch risk. | ## Recommendation **APPROVE** — The PR cleanly integrates SAP AI Core as a provider, adds appropriate flags and validation in the CLI, updates the composite action and CI workflow, and provides thorough unit tests covering token handling, deployment discovery, and both Anthropic and OpenAI paths. The concurrency and error-handling in the AI Core client are sensible, with token caching and truncated error bodies. Approve as is. For follow-up improvements, remove the duplicated comment in aicore.go, consider refreshing the token on 401 responses from AI Core endpoints, and potentially omit the "model" field for OpenAI requests through AI Core deployments to avoid configuration drift. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 6c2b36e2*
llm/aicore.go Outdated
@@ -0,0 +167,4 @@
// Fetch token first (before acquiring write lock to avoid holding lock during I/O)
token, err = c.getToken(ctx)
if err != nil {
First-time contributor

[NIT] Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed."). Remove the repeated line to improve readability.

**[NIT]** Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed."). Remove the repeated line to improve readability.
llm/aicore.go Outdated
@@ -0,0 +270,4 @@
reqBody := anthropicRequest{
AnthropicVersion: "bedrock-2023-05-31", // SAP AI Core uses Bedrock format
// Model omitted - AI Core deployment already specifies model
MaxTokens: maxTokens,
First-time contributor

[MINOR] Consider handling 401 Unauthorized responses by refreshing the OAuth token and retrying once. This would improve resilience if the token expires earlier than anticipated despite the 5-minute refresh buffer.

**[MINOR]** Consider handling 401 Unauthorized responses by refreshing the OAuth token and retrying once. This would improve resilience if the token expires earlier than anticipated despite the 5-minute refresh buffer.
llm/aicore.go Outdated
@@ -0,0 +342,4 @@
Temperature: temperature,
Messages: messages,
}
First-time contributor

[MINOR] In CompleteOpenAI, the request body includes the "model" field even though AI Core deployments already bind the model. Consider omitting Model to rely solely on the deployment configuration, reducing potential mismatch risk.

**[MINOR]** In CompleteOpenAI, the request body includes the "model" field even though AI Core deployments already bind the model. Consider omitting Model to rely solely on the deployment configuration, reducing potential mismatch risk.
security-review-bot requested review from security-review-bot 2026-05-10 15:40:22 +00:00
security-review-bot approved these changes 2026-05-10 15:40:23 +00:00
Dismissed
security-review-bot left a comment
Collaborator

Security Review

Summary

Overall, the AI Core integration is implemented carefully with OAuth2, bounded error logging, and concurrency-safe token management. CI has passed and no exploitable issues were found, though a couple of defense-in-depth improvements are recommended.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 250 The code trusts the deploymentUrl returned by the AI Core API and uses it directly for subsequent requests (e.g., appending /invoke or /chat/completions). Without validating that the URL uses HTTPS and matches the expected AI Core domain, this introduces a potential SSRF vector if the upstream were compromised or misconfigured. Consider validating the scheme is https and that the host matches (or is a subdomain of) the configured API URL host before using it.
2 [MINOR] llm/aicore.go 120 Auth and API endpoints from configuration (AuthURL, APIURL) are used without scheme/host validation. Misconfiguration could allow plaintext HTTP or an unexpected host. Consider enforcing https scheme and optionally restricting to an allowed domain list to reduce MITM risk.

Recommendation

APPROVE — The AI Core provider integration is solid and CI is green, so this can be merged. For hardening, validate that deployment URLs returned by AI Core use HTTPS and match the configured API host to reduce SSRF risk, and enforce HTTPS in AuthURL/APIURL configurations. These changes are defense-in-depth and not blockers for this PR.


Review by security


Evaluated against 6c2b36e2

# Security Review ## Summary Overall, the AI Core integration is implemented carefully with OAuth2, bounded error logging, and concurrency-safe token management. CI has passed and no exploitable issues were found, though a couple of defense-in-depth improvements are recommended. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 250 | The code trusts the deploymentUrl returned by the AI Core API and uses it directly for subsequent requests (e.g., appending /invoke or /chat/completions). Without validating that the URL uses HTTPS and matches the expected AI Core domain, this introduces a potential SSRF vector if the upstream were compromised or misconfigured. Consider validating the scheme is https and that the host matches (or is a subdomain of) the configured API URL host before using it. | | 2 | [MINOR] | `llm/aicore.go` | 120 | Auth and API endpoints from configuration (AuthURL, APIURL) are used without scheme/host validation. Misconfiguration could allow plaintext HTTP or an unexpected host. Consider enforcing https scheme and optionally restricting to an allowed domain list to reduce MITM risk. | ## Recommendation **APPROVE** — The AI Core provider integration is solid and CI is green, so this can be merged. For hardening, validate that deployment URLs returned by AI Core use HTTPS and match the configured API host to reduce SSRF risk, and enforce HTTPS in AuthURL/APIURL configurations. These changes are defense-in-depth and not blockers for this PR. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against 6c2b36e2*
llm/aicore.go Outdated
@@ -0,0 +117,4 @@
if err != nil {
return "", time.Time{}, fmt.Errorf("token request: %w", err)
}
defer resp.Body.Close()
Collaborator

[MINOR] Auth and API endpoints from configuration (AuthURL, APIURL) are used without scheme/host validation. Misconfiguration could allow plaintext HTTP or an unexpected host. Consider enforcing https scheme and optionally restricting to an allowed domain list to reduce MITM risk.

**[MINOR]** Auth and API endpoints from configuration (AuthURL, APIURL) are used without scheme/host validation. Misconfiguration could allow plaintext HTTP or an unexpected host. Consider enforcing https scheme and optionally restricting to an allowed domain list to reduce MITM risk.
llm/aicore.go Outdated
@@ -0,0 +247,4 @@
}
// CompleteAnthropic sends a request to an Anthropic model via AI Core.
func (c *AICoreClient) CompleteAnthropic(ctx context.Context, model string, messages []Message, maxTokens int, temperature float64) (string, error) {
Collaborator

[MINOR] The code trusts the deploymentUrl returned by the AI Core API and uses it directly for subsequent requests (e.g., appending /invoke or /chat/completions). Without validating that the URL uses HTTPS and matches the expected AI Core domain, this introduces a potential SSRF vector if the upstream were compromised or misconfigured. Consider validating the scheme is https and that the host matches (or is a subdomain of) the configured API URL host before using it.

**[MINOR]** The code trusts the deploymentUrl returned by the AI Core API and uses it directly for subsequent requests (e.g., appending /invoke or /chat/completions). Without validating that the URL uses HTTPS and matches the expected AI Core domain, this introduces a potential SSRF vector if the upstream were compromised or misconfigured. Consider validating the scheme is https and that the host matches (or is a subdomain of) the configured API URL host before using it.
gpt-review-bot approved these changes 2026-05-10 15:40:33 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Gpt Review

Summary

Solid addition of native SAP AI Core support with clear separation of concerns, thorough flag/env validation, and comprehensive tests. The implementation follows idiomatic Go patterns (concurrency, error handling, nil-opts) and integrates cleanly with the existing provider abstraction.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 260 CompleteOpenAI includes the model field in the request body even though the deployment already specifies the model; some AI Core/OpenAI-compatible endpoints may ignore or expect this differently. Consider omitting the model or documenting that it must match the deployment name to avoid mismatch issues.
2 [NIT] llm/aicore.go 190 Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.") appears twice; remove the repeated line for clarity.
3 [NIT] llm/aicore_test.go 410 Test name "TestClient_WithTimeout_PropagatestoAICore" has a small typo/formatting issue; consider renaming to "TestClient_WithTimeout_PropagatesToAICore" for readability and consistency.

Recommendation

APPROVE — The changes are well-designed and tested, with clean integration of the AI Core provider and appropriate validations and documentation updates. The minor issues identified are non-blocking and can be addressed in a follow-up: remove the duplicate comment in aicore.go, correct the test name typo, and consider clarifying or omitting the model field in the AI Core OpenAI request to ensure alignment with deployment-configured models. Overall, this is ready to merge.


Review by gpt


Evaluated against 6c2b36e2

# Gpt Review ## Summary Solid addition of native SAP AI Core support with clear separation of concerns, thorough flag/env validation, and comprehensive tests. The implementation follows idiomatic Go patterns (concurrency, error handling, nil-opts) and integrates cleanly with the existing provider abstraction. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 260 | CompleteOpenAI includes the model field in the request body even though the deployment already specifies the model; some AI Core/OpenAI-compatible endpoints may ignore or expect this differently. Consider omitting the model or documenting that it must match the deployment name to avoid mismatch issues. | | 2 | [NIT] | `llm/aicore.go` | 190 | Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.") appears twice; remove the repeated line for clarity. | | 3 | [NIT] | `llm/aicore_test.go` | 410 | Test name "TestClient_WithTimeout_PropagatestoAICore" has a small typo/formatting issue; consider renaming to "TestClient_WithTimeout_PropagatesToAICore" for readability and consistency. | ## Recommendation **APPROVE** — The changes are well-designed and tested, with clean integration of the AI Core provider and appropriate validations and documentation updates. The minor issues identified are non-blocking and can be addressed in a follow-up: remove the duplicate comment in aicore.go, correct the test name typo, and consider clarifying or omitting the model field in the AI Core OpenAI request to ensure alignment with deployment-configured models. Overall, this is ready to merge. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 6c2b36e2*
llm/aicore.go Outdated
@@ -0,0 +187,4 @@
return u, token, nil
}
return "", "", fmt.Errorf("no deployment found for model %q", model)
}
First-time contributor

[NIT] Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.") appears twice; remove the repeated line for clarity.

**[NIT]** Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.") appears twice; remove the repeated line for clarity.
llm/aicore.go Outdated
@@ -0,0 +257,4 @@
var system string
var userMessages []anthropicMsg
for _, m := range messages {
if m.Role == "system" {
First-time contributor

[MINOR] CompleteOpenAI includes the model field in the request body even though the deployment already specifies the model; some AI Core/OpenAI-compatible endpoints may ignore or expect this differently. Consider omitting the model or documenting that it must match the deployment name to avoid mismatch issues.

**[MINOR]** CompleteOpenAI includes the model field in the request body even though the deployment already specifies the model; some AI Core/OpenAI-compatible endpoints may ignore or expect this differently. Consider omitting the model or documenting that it must match the deployment name to avoid mismatch issues.
@@ -0,0 +407,4 @@
})
// Default timeout is 5 minutes
if client.http.Timeout != 5*time.Minute {
First-time contributor

[NIT] Test name "TestClient_WithTimeout_PropagatestoAICore" has a small typo/formatting issue; consider renaming to "TestClient_WithTimeout_PropagatesToAICore" for readability and consistency.

**[NIT]** Test name "TestClient_WithTimeout_PropagatestoAICore" has a small typo/formatting issue; consider renaming to "TestClient_WithTimeout_PropagatesToAICore" for readability and consistency.
security-review-bot requested review from security-review-bot 2026-05-10 15:40:57 +00:00
security-review-bot approved these changes 2026-05-10 15:40:57 +00:00
security-review-bot left a comment
Collaborator

Security Review

Summary

The PR adds a native SAP AI Core client with OAuth2, deployment discovery, and provider wiring, while updating CI and action inputs. The implementation uses context-aware HTTP calls, enforces timeouts, avoids logging secrets, and truncates upstream error bodies, with no exploitable issues identified.

Recommendation

APPROVE — Overall, the AI Core integration appears secure and well-structured: OAuth tokens are obtained via client credentials without being logged, token refresh is synchronized, requests are made with explicit timeouts and contexts, and upstream error bodies are truncated to limit information leakage. The composite action and CI workflow correctly source AI Core credentials from secrets. As a general hardening note (outside this change), consider applying similar body truncation to non-AI Core provider error paths to minimize potential information exposure if upstream services return verbose error payloads. No changes are required for this PR.


Review by security


Evaluated against 6c2b36e2

# Security Review ## Summary The PR adds a native SAP AI Core client with OAuth2, deployment discovery, and provider wiring, while updating CI and action inputs. The implementation uses context-aware HTTP calls, enforces timeouts, avoids logging secrets, and truncates upstream error bodies, with no exploitable issues identified. ## Recommendation **APPROVE** — Overall, the AI Core integration appears secure and well-structured: OAuth tokens are obtained via client credentials without being logged, token refresh is synchronized, requests are made with explicit timeouts and contexts, and upstream error bodies are truncated to limit information leakage. The composite action and CI workflow correctly source AI Core credentials from secrets. As a general hardening note (outside this change), consider applying similar body truncation to non-AI Core provider error paths to minimize potential information exposure if upstream services return verbose error payloads. No changes are required for this PR. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against 6c2b36e2*
rodin force-pushed feat/aicore-provider-v2 from 6c2b36e29d to 406f01a45c 2026-05-10 15:42:21 +00:00 Compare
sonnet-review-bot approved these changes 2026-05-10 15:43:10 +00:00
sonnet-review-bot left a comment
First-time contributor

Sonnet Review

Summary

This PR adds a well-structured SAP AI Core provider with OAuth2 token caching, deployment discovery, and routing to both Anthropic and OpenAI endpoints. The implementation follows established patterns, includes comprehensive tests, and CI passes. A few minor issues exist but none are blocking.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 155 Duplicate doc comment on getDeploymentURL. The function has two consecutive doc comment blocks starting with '// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.' The first is a stale copy that should be removed. This is visible in both the diff and the full file.
2 [MINOR] llm/aicore.go 233 fetchDeployments is called while holding the write lock (c.mu.Lock()), and it performs network I/O. Although the design comment acknowledges the pre-fetch token strategy, fetchDeployments itself still executes an HTTP request under the lock. This means concurrent callers waiting for the lock are blocked during the full deployment discovery round-trip. For CI usage this is acceptable (comment in code says so), but worth noting for any future longer-lived use.
3 [NIT] llm/client.go 214 The anthropicRequest struct alignment was changed: AnthropicVersion was added and Model changed from json:"model" to json:"model,omitempty". This is correct for the AI Core path (model is omitted because the deployment already specifies it), but the direct Anthropic path in completeAnthropic relies on Model being present. Since completeAnthropic always sets reqBody.Model = c.model, the omitempty will never trigger for a non-empty model — so it's functionally correct, but slightly confusing that the same struct now has different semantics for the two code paths.
4 [NIT] llm/aicore.go 57 The field name http on AICoreClient (and Client) shadows the standard library package name net/http. While this compiles fine and is a common pattern in the stdlib itself (e.g. http.Client), it can be slightly surprising. Not blocking.
5 [NIT] llm/aicore_test.go 1 The time import is used only in TestAICoreClient_TokenExpiry and TestAICoreClient_WithTimeout. The fmt import is used only in TestAICoreClient_TokenExpiry. These are fine — just noting the test file is in package llm (white-box), which is appropriate for testing unexported methods like getToken and getDeploymentURL.

Recommendation

APPROVE — Approve. The implementation is solid: correct double-checked locking for token caching, clean separation between the AI Core client and the generic Client, proper error truncation to avoid leaking credentials, and thorough test coverage including timeout propagation, token expiry, and both Anthropic and OpenAI routing paths. The only actionable fix is removing the duplicate doc comment on getDeploymentURL (line ~155 in aicore.go). The other findings are informational or cosmetic.


Review by sonnet


Evaluated against 406f01a4

# Sonnet Review ## Summary This PR adds a well-structured SAP AI Core provider with OAuth2 token caching, deployment discovery, and routing to both Anthropic and OpenAI endpoints. The implementation follows established patterns, includes comprehensive tests, and CI passes. A few minor issues exist but none are blocking. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 155 | Duplicate doc comment on `getDeploymentURL`. The function has two consecutive doc comment blocks starting with '// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.' The first is a stale copy that should be removed. This is visible in both the diff and the full file. | | 2 | [MINOR] | `llm/aicore.go` | 233 | `fetchDeployments` is called while holding the write lock (`c.mu.Lock()`), and it performs network I/O. Although the design comment acknowledges the pre-fetch token strategy, `fetchDeployments` itself still executes an HTTP request under the lock. This means concurrent callers waiting for the lock are blocked during the full deployment discovery round-trip. For CI usage this is acceptable (comment in code says so), but worth noting for any future longer-lived use. | | 3 | [NIT] | `llm/client.go` | 214 | The `anthropicRequest` struct alignment was changed: `AnthropicVersion` was added and `Model` changed from `json:"model"` to `json:"model,omitempty"`. This is correct for the AI Core path (model is omitted because the deployment already specifies it), but the direct Anthropic path in `completeAnthropic` relies on `Model` being present. Since `completeAnthropic` always sets `reqBody.Model = c.model`, the `omitempty` will never trigger for a non-empty model — so it's functionally correct, but slightly confusing that the same struct now has different semantics for the two code paths. | | 4 | [NIT] | `llm/aicore.go` | 57 | The field name `http` on `AICoreClient` (and `Client`) shadows the standard library package name `net/http`. While this compiles fine and is a common pattern in the stdlib itself (e.g. `http.Client`), it can be slightly surprising. Not blocking. | | 5 | [NIT] | `llm/aicore_test.go` | 1 | The `time` import is used only in `TestAICoreClient_TokenExpiry` and `TestAICoreClient_WithTimeout`. The `fmt` import is used only in `TestAICoreClient_TokenExpiry`. These are fine — just noting the test file is in `package llm` (white-box), which is appropriate for testing unexported methods like `getToken` and `getDeploymentURL`. | ## Recommendation **APPROVE** — Approve. The implementation is solid: correct double-checked locking for token caching, clean separation between the AI Core client and the generic Client, proper error truncation to avoid leaking credentials, and thorough test coverage including timeout propagation, token expiry, and both Anthropic and OpenAI routing paths. The only actionable fix is removing the duplicate doc comment on `getDeploymentURL` (line ~155 in aicore.go). The other findings are informational or cosmetic. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 406f01a4*
llm/aicore.go Outdated
@@ -0,0 +54,4 @@
http: &http.Client{Timeout: 5 * time.Minute},
deployments: make(map[string]string),
}
}
First-time contributor

[NIT] The field name http on AICoreClient (and Client) shadows the standard library package name net/http. While this compiles fine and is a common pattern in the stdlib itself (e.g. http.Client), it can be slightly surprising. Not blocking.

**[NIT]** The field name `http` on `AICoreClient` (and `Client`) shadows the standard library package name `net/http`. While this compiles fine and is a common pattern in the stdlib itself (e.g. `http.Client`), it can be slightly surprising. Not blocking.
llm/aicore.go Outdated
@@ -0,0 +152,4 @@
// during network I/O. In rare cases where multiple goroutines race and one waits a long
// time for the write lock, the token could theoretically expire. The 5-minute refresh
// buffer in getToken makes this extremely unlikely in practice.
func (c *AICoreClient) getDeploymentURL(ctx context.Context, model string) (deployURL, token string, err error) {
First-time contributor

[MINOR] Duplicate doc comment on getDeploymentURL. The function has two consecutive doc comment blocks starting with '// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.' The first is a stale copy that should be removed. This is visible in both the diff and the full file.

**[MINOR]** Duplicate doc comment on `getDeploymentURL`. The function has two consecutive doc comment blocks starting with '// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.' The first is a stale copy that should be removed. This is visible in both the diff and the full file.
llm/aicore.go Outdated
@@ -0,0 +230,4 @@
}
if err := json.Unmarshal(body, &deployResp); err != nil {
return fmt.Errorf("parse deployments response: %w", err)
}
First-time contributor

[MINOR] fetchDeployments is called while holding the write lock (c.mu.Lock()), and it performs network I/O. Although the design comment acknowledges the pre-fetch token strategy, fetchDeployments itself still executes an HTTP request under the lock. This means concurrent callers waiting for the lock are blocked during the full deployment discovery round-trip. For CI usage this is acceptable (comment in code says so), but worth noting for any future longer-lived use.

**[MINOR]** `fetchDeployments` is called while holding the write lock (`c.mu.Lock()`), and it performs network I/O. Although the design comment acknowledges the pre-fetch token strategy, `fetchDeployments` itself still executes an HTTP request under the lock. This means concurrent callers waiting for the lock are blocked during the full deployment discovery round-trip. For CI usage this is acceptable (comment in code says so), but worth noting for any future longer-lived use.
@@ -0,0 +1,535 @@
package llm
First-time contributor

[NIT] The time import is used only in TestAICoreClient_TokenExpiry and TestAICoreClient_WithTimeout. The fmt import is used only in TestAICoreClient_TokenExpiry. These are fine — just noting the test file is in package llm (white-box), which is appropriate for testing unexported methods like getToken and getDeploymentURL.

**[NIT]** The `time` import is used only in `TestAICoreClient_TokenExpiry` and `TestAICoreClient_WithTimeout`. The `fmt` import is used only in `TestAICoreClient_TokenExpiry`. These are fine — just noting the test file is in `package llm` (white-box), which is appropriate for testing unexported methods like `getToken` and `getDeploymentURL`.
llm/client.go Outdated
@@ -184,0 +211,4 @@
MaxTokens int `json:"max_tokens"`
System string `json:"system,omitempty"`
Messages []anthropicMsg `json:"messages"`
Temperature float64 `json:"temperature,omitempty"`
First-time contributor

[NIT] The anthropicRequest struct alignment was changed: AnthropicVersion was added and Model changed from json:"model" to json:"model,omitempty". This is correct for the AI Core path (model is omitted because the deployment already specifies it), but the direct Anthropic path in completeAnthropic relies on Model being present. Since completeAnthropic always sets reqBody.Model = c.model, the omitempty will never trigger for a non-empty model — so it's functionally correct, but slightly confusing that the same struct now has different semantics for the two code paths.

**[NIT]** The `anthropicRequest` struct alignment was changed: `AnthropicVersion` was added and `Model` changed from `json:"model"` to `json:"model,omitempty"`. This is correct for the AI Core path (model is omitted because the deployment already specifies it), but the direct Anthropic path in `completeAnthropic` relies on `Model` being present. Since `completeAnthropic` always sets `reqBody.Model = c.model`, the `omitempty` will never trigger for a non-empty model — so it's functionally correct, but slightly confusing that the same struct now has different semantics for the two code paths.
gpt-review-bot approved these changes 2026-05-10 15:44:19 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Gpt Review

Summary

Solid addition of native SAP AI Core support with clean integration into the CLI, composite action, and CI. The new AICore client is thread-safe, handles token refresh and deployment discovery, and includes comprehensive tests. CI has passed.

Recommendation

APPROVE — The AI Core implementation follows good patterns: concurrency-safe token caching, deployment discovery with a clear cache policy, context-aware HTTP calls, and careful error messages with truncated bodies. The CLI validation correctly differentiates provider requirements, and the composite action/CI updates look consistent with the new provider. Tests cover token handling, deployment resolution, Anthropic/OpenAI request routing, and timeout propagation. Consider these small future improvements: (1) make the OpenAI API version for AI Core configurable (env/flag) to ease upgrades; (2) optionally reuse a common response handling path (e.g., body length checks) in the AI Core client similar to doRequest; and (3) minor nits like cleaning duplicate comments and test function name typos. Overall, this is a well-executed feature — approve as is.


Review by gpt


Evaluated against 406f01a4

# Gpt Review ## Summary Solid addition of native SAP AI Core support with clean integration into the CLI, composite action, and CI. The new AICore client is thread-safe, handles token refresh and deployment discovery, and includes comprehensive tests. CI has passed. ## Recommendation **APPROVE** — The AI Core implementation follows good patterns: concurrency-safe token caching, deployment discovery with a clear cache policy, context-aware HTTP calls, and careful error messages with truncated bodies. The CLI validation correctly differentiates provider requirements, and the composite action/CI updates look consistent with the new provider. Tests cover token handling, deployment resolution, Anthropic/OpenAI request routing, and timeout propagation. Consider these small future improvements: (1) make the OpenAI API version for AI Core configurable (env/flag) to ease upgrades; (2) optionally reuse a common response handling path (e.g., body length checks) in the AI Core client similar to doRequest; and (3) minor nits like cleaning duplicate comments and test function name typos. Overall, this is a well-executed feature — approve as is. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 406f01a4*
rodin force-pushed feat/aicore-provider-v2 from 406f01a45c to abcb982599 2026-05-10 15:44:48 +00:00 Compare
Author
Owner

Self-review: PASS

Branch: feat/aicore-provider-v2
Date: 2026-05-10

Verification

  • Tests pass locally (go test ./... - all PASS)
  • No uncommitted changes
  • go vet clean
  • gofmt clean

Coherence

The change is coherent: native SAP AI Core provider with OAuth2 token caching, deployment discovery, and Anthropic/OpenAI routing. Single purpose, tells a clear story.

Fit

  • Uses established patterns: double-checked locking, fmt.Errorf("%w") error wrapping, httptest mocking
  • Builder pattern (WithAICore) matches existing WithProvider/WithTemperature
  • Validation logic in main.go follows the existing required-fields pattern

Solution

  • OAuth token caching with 5-minute refresh buffer is appropriate
  • Deployment cache without TTL is acceptable for short-lived CI processes (documented in code)
  • Model routing via anthropic-- prefix matches SAP AI Core naming convention

Completeness

  • Comprehensive tests (535 lines) covering token caching, expiry, deployment discovery, both Anthropic and OpenAI paths
  • README updated with AI Core example and input table
  • CI workflow updated for aicore provider
  • Error body truncation addresses security feedback

Issues Found

None blocking. Minor findings from bot reviews (unchecked json.Encode in tests, http field shadowing stdlib, hardcoded maxTokens=8192) are acceptable tradeoffs for the use case and consistent with existing code patterns.


Ready for human review.

## Self-review: PASS **Branch:** feat/aicore-provider-v2 **Date:** 2026-05-10 ### Verification - [x] Tests pass locally (`go test ./...` - all PASS) - [x] No uncommitted changes - [x] `go vet` clean - [x] `gofmt` clean ### Coherence The change is coherent: native SAP AI Core provider with OAuth2 token caching, deployment discovery, and Anthropic/OpenAI routing. Single purpose, tells a clear story. ### Fit - Uses established patterns: double-checked locking, `fmt.Errorf("%w")` error wrapping, httptest mocking - Builder pattern (`WithAICore`) matches existing `WithProvider`/`WithTemperature` - Validation logic in main.go follows the existing required-fields pattern ### Solution - OAuth token caching with 5-minute refresh buffer is appropriate - Deployment cache without TTL is acceptable for short-lived CI processes (documented in code) - Model routing via `anthropic--` prefix matches SAP AI Core naming convention ### Completeness - Comprehensive tests (535 lines) covering token caching, expiry, deployment discovery, both Anthropic and OpenAI paths - README updated with AI Core example and input table - CI workflow updated for aicore provider - Error body truncation addresses security feedback ### Issues Found None blocking. Minor findings from bot reviews (unchecked json.Encode in tests, `http` field shadowing stdlib, hardcoded maxTokens=8192) are acceptable tradeoffs for the use case and consistent with existing code patterns. --- **Ready for human review.**
rodin added the self-reviewed label 2026-05-10 15:45:21 +00:00
sonnet-review-bot approved these changes 2026-05-10 15:45:40 +00:00
sonnet-review-bot left a comment
First-time contributor

Sonnet Review

Summary

This PR adds a well-implemented native SAP AI Core provider with proper OAuth2 token caching, deployment discovery, and routing between Anthropic and OpenAI model paths. The code is idiomatic Go, follows the repository conventions (stdlib-only, proper error wrapping, table-driven tests), and CI passes. There are a few minor issues worth noting but none are blockers.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 153 Duplicate doc comment on getDeploymentURL. The function has two consecutive doc comment blocks — one starting 'getDeploymentURL returns the deployment URL...' appears twice. The second block is the authoritative one (it includes the token-return note and the concurrency caveat), but the first redundant line should be removed.
2 [MINOR] llm/aicore.go 230 The maxTokens parameter in CompleteAnthropic is part of the public signature but the only caller (completeAICore in client.go) always passes the hardcoded value 8192. This leaks an implementation detail into the public API. Consider making maxTokens an internal constant or adding it to AICoreConfig, since callers currently have no way to control it through the Client interface.
3 [MINOR] llm/aicore.go 178 When the deployment cache lookup succeeds on the RLock path, getToken is called again (acquiring locks again), even though a fresh token was already obtained if this was just populated. This is a minor inefficiency — not a correctness issue — but the fast-path could return the already-fetched token from the cache population path. The current structure is correct but slightly suboptimal.
4 [NIT] llm/aicore_test.go 395 TestAICoreClient_TokenExpiry sets expires_in: 1 in the server response but never actually waits for the token to expire naturally — it directly manipulates client.tokenExpiry. The server-side expires_in: 1 is misleading; it could just be any value since the test bypasses natural expiry. Consider using a comment or a more clearly intentional value (e.g., 3600) to avoid confusion.
5 [NIT] llm/aicore_test.go 261 In TestAICoreClient_DeploymentFetch, when two RUNNING deployments exist for gpt-5, the test asserts the second one (deploy-789) is returned. The current implementation happens to return deploy-789 because it overwrites deploy-456 during iteration, but this is iteration-order dependent on the JSON response. The test comment 'Should find running gpt-5, not stopped one' is accurate, but the assertion on the exact URL (deploy-789 vs deploy-123 for the other case) implicitly depends on the map-overwrite behavior. This is fine given the deterministic JSON array order from the test server, but worth a comment.
6 [NIT] cmd/review-bot/main.go 148 The isAICore variable is computed at line 94 before flag parsing conceptually completes (flag.Parse() is at line 82, so it's fine), but the comment '// Validate required fields' block is now split in two places — the early check and then the switch below. The isAICore variable is also used implicitly for provider routing. Consider consolidating or at least adding a comment noting that isAICore is derived from the already-parsed flag.

Recommendation

APPROVE — Approve with minor notes. The implementation is solid: the double-checked locking pattern for token refresh is correct, error messages are properly truncated to avoid leaking sensitive data, tests cover the key paths including token caching/expiry and deployment routing, and the CI integration changes are clean. The duplicate doc comment on getDeploymentURL should be fixed before merging (trivial cleanup). The maxTokens hardcoding in the public API surface is a minor design issue worth addressing in a follow-up. All other findings are nits.


Review by sonnet


Evaluated against abcb9825

# Sonnet Review ## Summary This PR adds a well-implemented native SAP AI Core provider with proper OAuth2 token caching, deployment discovery, and routing between Anthropic and OpenAI model paths. The code is idiomatic Go, follows the repository conventions (stdlib-only, proper error wrapping, table-driven tests), and CI passes. There are a few minor issues worth noting but none are blockers. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 153 | Duplicate doc comment on `getDeploymentURL`. The function has two consecutive doc comment blocks — one starting 'getDeploymentURL returns the deployment URL...' appears twice. The second block is the authoritative one (it includes the token-return note and the concurrency caveat), but the first redundant line should be removed. | | 2 | [MINOR] | `llm/aicore.go` | 230 | The `maxTokens` parameter in `CompleteAnthropic` is part of the public signature but the only caller (`completeAICore` in client.go) always passes the hardcoded value 8192. This leaks an implementation detail into the public API. Consider making `maxTokens` an internal constant or adding it to `AICoreConfig`, since callers currently have no way to control it through the `Client` interface. | | 3 | [MINOR] | `llm/aicore.go` | 178 | When the deployment cache lookup succeeds on the RLock path, `getToken` is called again (acquiring locks again), even though a fresh token was already obtained if this was just populated. This is a minor inefficiency — not a correctness issue — but the fast-path could return the already-fetched token from the cache population path. The current structure is correct but slightly suboptimal. | | 4 | [NIT] | `llm/aicore_test.go` | 395 | `TestAICoreClient_TokenExpiry` sets `expires_in: 1` in the server response but never actually waits for the token to expire naturally — it directly manipulates `client.tokenExpiry`. The server-side `expires_in: 1` is misleading; it could just be any value since the test bypasses natural expiry. Consider using a comment or a more clearly intentional value (e.g., 3600) to avoid confusion. | | 5 | [NIT] | `llm/aicore_test.go` | 261 | In `TestAICoreClient_DeploymentFetch`, when two RUNNING deployments exist for `gpt-5`, the test asserts the second one (`deploy-789`) is returned. The current implementation happens to return `deploy-789` because it overwrites `deploy-456` during iteration, but this is iteration-order dependent on the JSON response. The test comment 'Should find running gpt-5, not stopped one' is accurate, but the assertion on the exact URL (`deploy-789` vs `deploy-123` for the other case) implicitly depends on the map-overwrite behavior. This is fine given the deterministic JSON array order from the test server, but worth a comment. | | 6 | [NIT] | `cmd/review-bot/main.go` | 148 | The `isAICore` variable is computed at line 94 before flag parsing conceptually completes (flag.Parse() is at line 82, so it's fine), but the comment '// Validate required fields' block is now split in two places — the early check and then the switch below. The `isAICore` variable is also used implicitly for provider routing. Consider consolidating or at least adding a comment noting that `isAICore` is derived from the already-parsed flag. | ## Recommendation **APPROVE** — Approve with minor notes. The implementation is solid: the double-checked locking pattern for token refresh is correct, error messages are properly truncated to avoid leaking sensitive data, tests cover the key paths including token caching/expiry and deployment routing, and the CI integration changes are clean. The duplicate doc comment on `getDeploymentURL` should be fixed before merging (trivial cleanup). The `maxTokens` hardcoding in the public API surface is a minor design issue worth addressing in a follow-up. All other findings are nits. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against abcb9825*
@@ -128,0 +145,4 @@
llmClient.WithAICore(llm.AICoreConfig{
ClientID: *aicoreClientID,
ClientSecret: *aicoreClientSecret,
AuthURL: *aicoreAuthURL,
First-time contributor

[NIT] The isAICore variable is computed at line 94 before flag parsing conceptually completes (flag.Parse() is at line 82, so it's fine), but the comment '// Validate required fields' block is now split in two places — the early check and then the switch below. The isAICore variable is also used implicitly for provider routing. Consider consolidating or at least adding a comment noting that isAICore is derived from the already-parsed flag.

**[NIT]** The `isAICore` variable is computed at line 94 before flag parsing conceptually completes (flag.Parse() is at line 82, so it's fine), but the comment '// Validate required fields' block is now split in two places — the early check and then the switch below. The `isAICore` variable is also used implicitly for provider routing. Consider consolidating or at least adding a comment noting that `isAICore` is derived from the already-parsed flag.
llm/aicore.go Outdated
@@ -0,0 +150,4 @@
//
// Note: The token is fetched before acquiring the write lock to avoid holding the lock
// during network I/O. In rare cases where multiple goroutines race and one waits a long
// time for the write lock, the token could theoretically expire. The 5-minute refresh
First-time contributor

[MINOR] Duplicate doc comment on getDeploymentURL. The function has two consecutive doc comment blocks — one starting 'getDeploymentURL returns the deployment URL...' appears twice. The second block is the authoritative one (it includes the token-return note and the concurrency caveat), but the first redundant line should be removed.

**[MINOR]** Duplicate doc comment on `getDeploymentURL`. The function has two consecutive doc comment blocks — one starting 'getDeploymentURL returns the deployment URL...' appears twice. The second block is the authoritative one (it includes the token-return note and the concurrency caveat), but the first redundant line should be removed.
llm/aicore.go Outdated
@@ -0,0 +175,4 @@
defer c.mu.Unlock()
// Double-check after acquiring write lock
if u, ok := c.deployments[model]; ok {
First-time contributor

[MINOR] When the deployment cache lookup succeeds on the RLock path, getToken is called again (acquiring locks again), even though a fresh token was already obtained if this was just populated. This is a minor inefficiency — not a correctness issue — but the fast-path could return the already-fetched token from the cache population path. The current structure is correct but slightly suboptimal.

**[MINOR]** When the deployment cache lookup succeeds on the RLock path, `getToken` is called again (acquiring locks again), even though a fresh token was already obtained if this was just populated. This is a minor inefficiency — not a correctness issue — but the fast-path could return the already-fetched token from the cache population path. The current structure is correct but slightly suboptimal.
llm/aicore.go Outdated
@@ -0,0 +227,4 @@
} `json:"resources"`
} `json:"details"`
} `json:"resources"`
}
First-time contributor

[MINOR] The maxTokens parameter in CompleteAnthropic is part of the public signature but the only caller (completeAICore in client.go) always passes the hardcoded value 8192. This leaks an implementation detail into the public API. Consider making maxTokens an internal constant or adding it to AICoreConfig, since callers currently have no way to control it through the Client interface.

**[MINOR]** The `maxTokens` parameter in `CompleteAnthropic` is part of the public signature but the only caller (`completeAICore` in client.go) always passes the hardcoded value 8192. This leaks an implementation detail into the public API. Consider making `maxTokens` an internal constant or adding it to `AICoreConfig`, since callers currently have no way to control it through the `Client` interface.
@@ -0,0 +258,4 @@
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"resources": []map[string]interface{}{
{
First-time contributor

[NIT] In TestAICoreClient_DeploymentFetch, when two RUNNING deployments exist for gpt-5, the test asserts the second one (deploy-789) is returned. The current implementation happens to return deploy-789 because it overwrites deploy-456 during iteration, but this is iteration-order dependent on the JSON response. The test comment 'Should find running gpt-5, not stopped one' is accurate, but the assertion on the exact URL (deploy-789 vs deploy-123 for the other case) implicitly depends on the map-overwrite behavior. This is fine given the deterministic JSON array order from the test server, but worth a comment.

**[NIT]** In `TestAICoreClient_DeploymentFetch`, when two RUNNING deployments exist for `gpt-5`, the test asserts the second one (`deploy-789`) is returned. The current implementation happens to return `deploy-789` because it overwrites `deploy-456` during iteration, but this is iteration-order dependent on the JSON response. The test comment 'Should find running gpt-5, not stopped one' is accurate, but the assertion on the exact URL (`deploy-789` vs `deploy-123` for the other case) implicitly depends on the map-overwrite behavior. This is fine given the deterministic JSON array order from the test server, but worth a comment.
@@ -0,0 +392,4 @@
if token1 == token2 {
t.Error("expected different tokens after expiry")
}
if atomic.LoadInt32(&tokenCalls) != 2 {
First-time contributor

[NIT] TestAICoreClient_TokenExpiry sets expires_in: 1 in the server response but never actually waits for the token to expire naturally — it directly manipulates client.tokenExpiry. The server-side expires_in: 1 is misleading; it could just be any value since the test bypasses natural expiry. Consider using a comment or a more clearly intentional value (e.g., 3600) to avoid confusion.

**[NIT]** `TestAICoreClient_TokenExpiry` sets `expires_in: 1` in the server response but never actually waits for the token to expire naturally — it directly manipulates `client.tokenExpiry`. The server-side `expires_in: 1` is misleading; it could just be any value since the test bypasses natural expiry. Consider using a comment or a more clearly intentional value (e.g., 3600) to avoid confusion.
rodin removed the self-reviewed label 2026-05-10 15:46:03 +00:00
gpt-review-bot approved these changes 2026-05-10 15:46:24 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Gpt Review

Summary

Solid addition of native SAP AI Core support with clear flag/env wiring, thorough tests, and CI/workflow/readme updates. Concurrency and token/deployment discovery logic are reasonable; only minor polish and a small concurrency improvement are suggested.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 210 getDeploymentURL holds the write lock while calling fetchDeployments, which performs network I/O. Holding a mutex during blocking I/O can stall other callers. Consider fetching deployments outside the lock (singleflight or double-checked locking) and only locking to update the cache.
2 [NIT] llm/aicore.go 150 Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.") appears twice. Remove the duplicate for clarity.
3 [NIT] llm/aicore.go 340 Exported function IsAnthropicModel lacks a doc comment. Add a brief comment (e.g., "IsAnthropicModel reports whether the model name corresponds to an Anthropic deployment in AI Core.") to follow Go doc conventions for exported identifiers.
4 [NIT] llm/aicore_test.go 445 Test name "TestClient_WithTimeout_PropagatestoAICore" has a small typo in the word separation ("Propagatesto"). Consider renaming to "TestClient_WithTimeout_PropagatesToAICore" for readability.
5 [NIT] cmd/review-bot/main.go 97 In aicore mode, llm.NewClient is constructed with potentially empty baseURL/apiKey. While harmless since the provider switches to AI Core, consider a small comment explaining this or a helper constructor to avoid confusion.

Recommendation

APPROVE — The implementation is clean and well-tested, with appropriate configuration flags, environment propagation, and CI workflow updates. The AI Core client correctly handles OAuth token refresh, deployment discovery, and routing between Anthropic (invoke with bedrock anthropic_version) and OpenAI-compatible endpoints with versioning. Approve as-is. For follow-ups, consider avoiding holding the write lock across network I/O in deployment fetch, remove the duplicate comment, add doc comments for exported helpers, and polish the test name typo. These are non-blocking improvements.


Review by gpt


Evaluated against abcb9825

# Gpt Review ## Summary Solid addition of native SAP AI Core support with clear flag/env wiring, thorough tests, and CI/workflow/readme updates. Concurrency and token/deployment discovery logic are reasonable; only minor polish and a small concurrency improvement are suggested. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 210 | getDeploymentURL holds the write lock while calling fetchDeployments, which performs network I/O. Holding a mutex during blocking I/O can stall other callers. Consider fetching deployments outside the lock (singleflight or double-checked locking) and only locking to update the cache. | | 2 | [NIT] | `llm/aicore.go` | 150 | Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.") appears twice. Remove the duplicate for clarity. | | 3 | [NIT] | `llm/aicore.go` | 340 | Exported function IsAnthropicModel lacks a doc comment. Add a brief comment (e.g., "IsAnthropicModel reports whether the model name corresponds to an Anthropic deployment in AI Core.") to follow Go doc conventions for exported identifiers. | | 4 | [NIT] | `llm/aicore_test.go` | 445 | Test name "TestClient_WithTimeout_PropagatestoAICore" has a small typo in the word separation ("Propagatesto"). Consider renaming to "TestClient_WithTimeout_PropagatesToAICore" for readability. | | 5 | [NIT] | `cmd/review-bot/main.go` | 97 | In aicore mode, llm.NewClient is constructed with potentially empty baseURL/apiKey. While harmless since the provider switches to AI Core, consider a small comment explaining this or a helper constructor to avoid confusion. | ## Recommendation **APPROVE** — The implementation is clean and well-tested, with appropriate configuration flags, environment propagation, and CI workflow updates. The AI Core client correctly handles OAuth token refresh, deployment discovery, and routing between Anthropic (invoke with bedrock anthropic_version) and OpenAI-compatible endpoints with versioning. Approve as-is. For follow-ups, consider avoiding holding the write lock across network I/O in deployment fetch, remove the duplicate comment, add doc comments for exported helpers, and polish the test name typo. These are non-blocking improvements. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against abcb9825*
@@ -89,2 +95,3 @@
if *giteaURL == "" || *repo == "" || *prNum == "" || *reviewerToken == "" || *llmModel == "" {
fmt.Fprintf(os.Stderr, "Error: missing required flags or environment variables\n\n")
fmt.Fprintf(os.Stderr, "Required: --gitea-url, --repo, --pr, --reviewer-token, --llm-base-url, --llm-api-key, --llm-model\n")
fmt.Fprintf(os.Stderr, "Required: --gitea-url, --repo, --pr, --reviewer-token, --llm-model\n")
First-time contributor

[NIT] In aicore mode, llm.NewClient is constructed with potentially empty baseURL/apiKey. While harmless since the provider switches to AI Core, consider a small comment explaining this or a helper constructor to avoid confusion.

**[NIT]** In aicore mode, llm.NewClient is constructed with potentially empty baseURL/apiKey. While harmless since the provider switches to AI Core, consider a small comment explaining this or a helper constructor to avoid confusion.
llm/aicore.go Outdated
@@ -0,0 +147,4 @@
// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.
// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.
// Also returns a valid token for use by the caller, avoiding redundant getToken calls.
//
First-time contributor

[NIT] Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.") appears twice. Remove the duplicate for clarity.

**[NIT]** Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.") appears twice. Remove the duplicate for clarity.
llm/aicore.go Outdated
@@ -0,0 +207,4 @@
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read deployments response: %w", err)
}
First-time contributor

[MINOR] getDeploymentURL holds the write lock while calling fetchDeployments, which performs network I/O. Holding a mutex during blocking I/O can stall other callers. Consider fetching deployments outside the lock (singleflight or double-checked locking) and only locking to update the cache.

**[MINOR]** getDeploymentURL holds the write lock while calling fetchDeployments, which performs network I/O. Holding a mutex during blocking I/O can stall other callers. Consider fetching deployments outside the lock (singleflight or double-checked locking) and only locking to update the cache.
llm/aicore.go Outdated
@@ -0,0 +337,4 @@
return "", err
}
reqBody := ChatRequest{
First-time contributor

[NIT] Exported function IsAnthropicModel lacks a doc comment. Add a brief comment (e.g., "IsAnthropicModel reports whether the model name corresponds to an Anthropic deployment in AI Core.") to follow Go doc conventions for exported identifiers.

**[NIT]** Exported function IsAnthropicModel lacks a doc comment. Add a brief comment (e.g., "IsAnthropicModel reports whether the model name corresponds to an Anthropic deployment in AI Core.") to follow Go doc conventions for exported identifiers.
@@ -0,0 +442,4 @@
func TestClient_WithTimeout_PropagatestoAICore(t *testing.T) {
client := NewClient("http://example.com", "key", "model").
WithAICore(AICoreConfig{
First-time contributor

[NIT] Test name "TestClient_WithTimeout_PropagatestoAICore" has a small typo in the word separation ("Propagatesto"). Consider renaming to "TestClient_WithTimeout_PropagatesToAICore" for readability.

**[NIT]** Test name "TestClient_WithTimeout_PropagatestoAICore" has a small typo in the word separation ("Propagatesto"). Consider renaming to "TestClient_WithTimeout_PropagatesToAICore" for readability.
security-review-bot requested review from security-review-bot 2026-05-10 15:47:36 +00:00
security-review-bot approved these changes 2026-05-10 15:47:36 +00:00
security-review-bot left a comment
Collaborator

Security Review

Summary

Overall, the additions for native SAP AI Core support are well-structured and security-conscious, with OAuth handling, deployment discovery, and careful path validation for local files. CI passed and I did not find exploitable vulnerabilities in the changes.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 93 AuthURL is used verbatim to build the token endpoint without enforcing HTTPS. To prevent accidental plaintext credential transmission, validate the scheme and reject non-HTTPS URLs during client configuration.
2 [MINOR] llm/aicore.go 176 APIURL is used verbatim to build the deployments endpoint without enforcing HTTPS. Add a scheme check to ensure only HTTPS endpoints are used.
3 [MINOR] llm/aicore.go 117 Error messages include (truncated) upstream response bodies from the OAuth token and AI Core endpoints. While truncated to 200 bytes, consider further limiting or redacting token endpoint bodies to avoid leaking potentially sensitive details in CI logs (also applies to lines 196, 291, and 356 for other AI Core requests).

Recommendation

APPROVE — Proceed with merge. The implementation correctly isolates SAP AI Core authentication, avoids logging secrets, honors timeouts, and validates system-prompt file paths against path traversal and symlinks. For defense-in-depth, add scheme validation to ensure AICore AuthURL and APIURL are HTTPS-only, and consider suppressing or further redacting token endpoint error bodies to minimize information leakage in logs. Otherwise, the changes are sound and CI is green.


Review by security


Evaluated against abcb9825

# Security Review ## Summary Overall, the additions for native SAP AI Core support are well-structured and security-conscious, with OAuth handling, deployment discovery, and careful path validation for local files. CI passed and I did not find exploitable vulnerabilities in the changes. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 93 | AuthURL is used verbatim to build the token endpoint without enforcing HTTPS. To prevent accidental plaintext credential transmission, validate the scheme and reject non-HTTPS URLs during client configuration. | | 2 | [MINOR] | `llm/aicore.go` | 176 | APIURL is used verbatim to build the deployments endpoint without enforcing HTTPS. Add a scheme check to ensure only HTTPS endpoints are used. | | 3 | [MINOR] | `llm/aicore.go` | 117 | Error messages include (truncated) upstream response bodies from the OAuth token and AI Core endpoints. While truncated to 200 bytes, consider further limiting or redacting token endpoint bodies to avoid leaking potentially sensitive details in CI logs (also applies to lines 196, 291, and 356 for other AI Core requests). | ## Recommendation **APPROVE** — Proceed with merge. The implementation correctly isolates SAP AI Core authentication, avoids logging secrets, honors timeouts, and validates system-prompt file paths against path traversal and symlinks. For defense-in-depth, add scheme validation to ensure AICore AuthURL and APIURL are HTTPS-only, and consider suppressing or further redacting token endpoint error bodies to minimize information leakage in logs. Otherwise, the changes are sound and CI is green. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against abcb9825*
llm/aicore.go Outdated
@@ -0,0 +90,4 @@
return c.token, nil
}
token, expiry, err := c.fetchToken(ctx)
Collaborator

[MINOR] AuthURL is used verbatim to build the token endpoint without enforcing HTTPS. To prevent accidental plaintext credential transmission, validate the scheme and reject non-HTTPS URLs during client configuration.

**[MINOR]** AuthURL is used verbatim to build the token endpoint without enforcing HTTPS. To prevent accidental plaintext credential transmission, validate the scheme and reject non-HTTPS URLs during client configuration.
llm/aicore.go Outdated
@@ -0,0 +114,4 @@
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := c.http.Do(req)
if err != nil {
Collaborator

[MINOR] Error messages include (truncated) upstream response bodies from the OAuth token and AI Core endpoints. While truncated to 200 bytes, consider further limiting or redacting token endpoint bodies to avoid leaking potentially sensitive details in CI logs (also applies to lines 196, 291, and 356 for other AI Core requests).

**[MINOR]** Error messages include (truncated) upstream response bodies from the OAuth token and AI Core endpoints. While truncated to 200 bytes, consider further limiting or redacting token endpoint bodies to avoid leaking potentially sensitive details in CI logs (also applies to lines 196, 291, and 356 for other AI Core requests).
llm/aicore.go Outdated
@@ -0,0 +173,4 @@
c.mu.Lock()
defer c.mu.Unlock()
Collaborator

[MINOR] APIURL is used verbatim to build the deployments endpoint without enforcing HTTPS. Add a scheme check to ensure only HTTPS endpoints are used.

**[MINOR]** APIURL is used verbatim to build the deployments endpoint without enforcing HTTPS. Add a scheme check to ensure only HTTPS endpoints are used.
rodin force-pushed feat/aicore-provider-v2 from abcb982599 to 7f16af2b5b 2026-05-10 15:47:46 +00:00 Compare
sonnet-review-bot approved these changes 2026-05-10 15:48:36 +00:00
sonnet-review-bot left a comment
First-time contributor

Sonnet Review

Summary

This PR adds a well-structured SAP AI Core native provider with proper OAuth2 token caching, deployment discovery, and routing between Anthropic and OpenAI model endpoints. The code follows Go conventions, has comprehensive test coverage, and CI passes. A few minor issues exist but none are blockers.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 148 Duplicate doc comment. The getDeploymentURL function has its doc comment written twice: the first instance at line 148 is a plain 'getDeploymentURL returns the deployment URL...' and then it repeats again at line 149. One should be removed.
2 [MINOR] llm/aicore.go 265 When there are multiple RUNNING deployments for the same model name, the last one in the list silently wins (map assignment overwrites). If SAP AI Core could return multiple running deployments for the same model, this could select an unexpected one. The behavior is undocumented; at minimum a log warning would be helpful.
3 [MINOR] llm/aicore.go 175 The fetchDeployments call inside getDeploymentURL is invoked while holding c.mu (write lock), and fetchDeployments calls c.http.Do which is a network I/O operation. The design comment acknowledges fetching the token outside the lock, but the deployment fetch itself still holds the lock during network I/O. This is fine for the intended short-lived CI use case (as noted in the struct doc), but it means concurrent callers waiting for other models will block during this I/O. The code comment could explicitly mention this constraint.
4 [MINOR] llm/aicore_test.go 390 The TestIsAnthropicModel test uses the old-style loop rather than t.Run subtests, making it harder to see which specific case failed. Per the testing patterns established in the codebase, table-driven tests should use t.Run with named subtests.
5 [NIT] llm/client.go 209 The anthropicRequest struct fields have inconsistent alignment after the addition of AnthropicVersion - the field names are left-aligned but the types are misaligned compared to the original struct. gofmt handles this automatically, so this is only relevant if gofmt wasn't run, but worth noting.
6 [NIT] llm/aicore.go 34 The struct field http *http.Client shadows the imported net/http package name within the struct's method set. While Go resolves this correctly (field access via c.http.Do vs package via http.NewRequestWithContext), naming the field httpClient would be clearer and avoid any reader confusion.
7 [NIT] cmd/review-bot/main.go 134 findOwnReview (singular) is defined but no longer used after this PR — findAllOwnReviews is used exclusively. This dead code should be removed.

Recommendation

APPROVE — APPROVE. The implementation is solid: correct double-checked locking pattern for token/deployment caching, proper use of sync.RWMutex, good error wrapping with context, comprehensive test coverage using httptest, and clean integration into the existing provider abstraction. The dead code (findOwnReview) and duplicate doc comment are the most concrete issues to clean up, but neither is a blocker. The design trade-off of holding the mutex during deployment fetch I/O is acceptable for the CI use case and is documented.


Review by sonnet


Evaluated against 7f16af2b

# Sonnet Review ## Summary This PR adds a well-structured SAP AI Core native provider with proper OAuth2 token caching, deployment discovery, and routing between Anthropic and OpenAI model endpoints. The code follows Go conventions, has comprehensive test coverage, and CI passes. A few minor issues exist but none are blockers. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 148 | Duplicate doc comment. The `getDeploymentURL` function has its doc comment written twice: the first instance at line 148 is a plain 'getDeploymentURL returns the deployment URL...' and then it repeats again at line 149. One should be removed. | | 2 | [MINOR] | `llm/aicore.go` | 265 | When there are multiple RUNNING deployments for the same model name, the last one in the list silently wins (map assignment overwrites). If SAP AI Core could return multiple running deployments for the same model, this could select an unexpected one. The behavior is undocumented; at minimum a log warning would be helpful. | | 3 | [MINOR] | `llm/aicore.go` | 175 | The `fetchDeployments` call inside `getDeploymentURL` is invoked while holding `c.mu` (write lock), and `fetchDeployments` calls `c.http.Do` which is a network I/O operation. The design comment acknowledges fetching the token outside the lock, but the deployment fetch itself still holds the lock during network I/O. This is fine for the intended short-lived CI use case (as noted in the struct doc), but it means concurrent callers waiting for other models will block during this I/O. The code comment could explicitly mention this constraint. | | 4 | [MINOR] | `llm/aicore_test.go` | 390 | The `TestIsAnthropicModel` test uses the old-style loop rather than `t.Run` subtests, making it harder to see which specific case failed. Per the testing patterns established in the codebase, table-driven tests should use `t.Run` with named subtests. | | 5 | [NIT] | `llm/client.go` | 209 | The `anthropicRequest` struct fields have inconsistent alignment after the addition of `AnthropicVersion` - the field names are left-aligned but the types are misaligned compared to the original struct. `gofmt` handles this automatically, so this is only relevant if `gofmt` wasn't run, but worth noting. | | 6 | [NIT] | `llm/aicore.go` | 34 | The struct field `http *http.Client` shadows the imported `net/http` package name within the struct's method set. While Go resolves this correctly (field access via `c.http.Do` vs package via `http.NewRequestWithContext`), naming the field `httpClient` would be clearer and avoid any reader confusion. | | 7 | [NIT] | `cmd/review-bot/main.go` | 134 | `findOwnReview` (singular) is defined but no longer used after this PR — `findAllOwnReviews` is used exclusively. This dead code should be removed. | ## Recommendation **APPROVE** — APPROVE. The implementation is solid: correct double-checked locking pattern for token/deployment caching, proper use of `sync.RWMutex`, good error wrapping with context, comprehensive test coverage using `httptest`, and clean integration into the existing provider abstraction. The dead code (`findOwnReview`) and duplicate doc comment are the most concrete issues to clean up, but neither is a blocker. The design trade-off of holding the mutex during deployment fetch I/O is acceptable for the CI use case and is documented. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 7f16af2b*
llm/aicore.go Outdated
@@ -0,0 +31,4 @@
}
// AICoreClient wraps AI Core authentication and deployment discovery.
// Thread-safe for concurrent use after construction.
First-time contributor

[NIT] The struct field http *http.Client shadows the imported net/http package name within the struct's method set. While Go resolves this correctly (field access via c.http.Do vs package via http.NewRequestWithContext), naming the field httpClient would be clearer and avoid any reader confusion.

**[NIT]** The struct field `http *http.Client` shadows the imported `net/http` package name within the struct's method set. While Go resolves this correctly (field access via `c.http.Do` vs package via `http.NewRequestWithContext`), naming the field `httpClient` would be clearer and avoid any reader confusion.
llm/aicore.go Outdated
@@ -0,0 +145,4 @@
}
// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.
// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.
First-time contributor

[MINOR] Duplicate doc comment. The getDeploymentURL function has its doc comment written twice: the first instance at line 148 is a plain 'getDeploymentURL returns the deployment URL...' and then it repeats again at line 149. One should be removed.

**[MINOR]** Duplicate doc comment. The `getDeploymentURL` function has its doc comment written twice: the first instance at line 148 is a plain 'getDeploymentURL returns the deployment URL...' and then it repeats again at line 149. One should be removed.
llm/aicore.go Outdated
@@ -0,0 +172,4 @@
}
c.mu.Lock()
defer c.mu.Unlock()
First-time contributor

[MINOR] The fetchDeployments call inside getDeploymentURL is invoked while holding c.mu (write lock), and fetchDeployments calls c.http.Do which is a network I/O operation. The design comment acknowledges fetching the token outside the lock, but the deployment fetch itself still holds the lock during network I/O. This is fine for the intended short-lived CI use case (as noted in the struct doc), but it means concurrent callers waiting for other models will block during this I/O. The code comment could explicitly mention this constraint.

**[MINOR]** The `fetchDeployments` call inside `getDeploymentURL` is invoked while holding `c.mu` (write lock), and `fetchDeployments` calls `c.http.Do` which is a network I/O operation. The design comment acknowledges fetching the token outside the lock, but the deployment fetch itself still holds the lock during network I/O. This is fine for the intended short-lived CI use case (as noted in the struct doc), but it means concurrent callers waiting for other models will block during this I/O. The code comment could explicitly mention this constraint.
llm/aicore.go Outdated
@@ -0,0 +262,4 @@
} else {
userMessages = append(userMessages, anthropicMsg{
Role: m.Role,
Content: m.Content,
First-time contributor

[MINOR] When there are multiple RUNNING deployments for the same model name, the last one in the list silently wins (map assignment overwrites). If SAP AI Core could return multiple running deployments for the same model, this could select an unexpected one. The behavior is undocumented; at minimum a log warning would be helpful.

**[MINOR]** When there are multiple RUNNING deployments for the same model name, the last one in the list silently wins (map assignment overwrites). If SAP AI Core could return multiple running deployments for the same model, this could select an unexpected one. The behavior is undocumented; at minimum a log warning would be helpful.
@@ -0,0 +387,4 @@
token2, err := client.getToken(context.Background())
if err != nil {
t.Fatalf("second getToken: %v", err)
}
First-time contributor

[MINOR] The TestIsAnthropicModel test uses the old-style loop rather than t.Run subtests, making it harder to see which specific case failed. Per the testing patterns established in the codebase, table-driven tests should use t.Run with named subtests.

**[MINOR]** The `TestIsAnthropicModel` test uses the old-style loop rather than `t.Run` subtests, making it harder to see which specific case failed. Per the testing patterns established in the codebase, table-driven tests should use `t.Run` with named subtests.
llm/client.go Outdated
@@ -181,3 +209,1 @@
System string `json:"system,omitempty"`
Messages []anthropicMsg `json:"messages"`
Temperature float64 `json:"temperature,omitempty"`
AnthropicVersion string `json:"anthropic_version,omitempty"`
First-time contributor

[NIT] The anthropicRequest struct fields have inconsistent alignment after the addition of AnthropicVersion - the field names are left-aligned but the types are misaligned compared to the original struct. gofmt handles this automatically, so this is only relevant if gofmt wasn't run, but worth noting.

**[NIT]** The `anthropicRequest` struct fields have inconsistent alignment after the addition of `AnthropicVersion` - the field names are left-aligned but the types are misaligned compared to the original struct. `gofmt` handles this automatically, so this is only relevant if `gofmt` wasn't run, but worth noting.
gpt-review-bot approved these changes 2026-05-10 15:49:08 +00:00
gpt-review-bot left a comment
First-time contributor

Gpt Review

Summary

Solid addition of native SAP AI Core support with clear flag/env wiring, safe OAuth handling, deployment discovery, and comprehensive tests. CI passed and the changes integrate cleanly without breaking existing providers.

Recommendation

APPROVE — The implementation is well-structured and aligns with repository conventions: errors are wrapped with context, concurrency is handled via RWMutex, and tests cover token refresh, deployment discovery, endpoint behavior, and client integration. The CLI and composite action changes correctly gate required credentials based on provider and avoid breaking existing OpenAI/Anthropic usage. Minor future refinements to consider (non-blocking for this PR): optionally adding a cache TTL for deployment discovery in long-lived scenarios, truncating non-AI Core error bodies in llm/client.go similar to AI Core to avoid verbose logs, and small doc/comment cleanups. Overall, this is ready to merge.


Review by gpt


Evaluated against 7f16af2b

# Gpt Review ## Summary Solid addition of native SAP AI Core support with clear flag/env wiring, safe OAuth handling, deployment discovery, and comprehensive tests. CI passed and the changes integrate cleanly without breaking existing providers. ## Recommendation **APPROVE** — The implementation is well-structured and aligns with repository conventions: errors are wrapped with context, concurrency is handled via RWMutex, and tests cover token refresh, deployment discovery, endpoint behavior, and client integration. The CLI and composite action changes correctly gate required credentials based on provider and avoid breaking existing OpenAI/Anthropic usage. Minor future refinements to consider (non-blocking for this PR): optionally adding a cache TTL for deployment discovery in long-lived scenarios, truncating non-AI Core error bodies in llm/client.go similar to AI Core to avoid verbose logs, and small doc/comment cleanups. Overall, this is ready to merge. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 7f16af2b*
security-review-bot requested review from security-review-bot 2026-05-10 15:49:24 +00:00
security-review-bot approved these changes 2026-05-10 15:49:24 +00:00
security-review-bot left a comment
Collaborator

Security Review

Summary

Overall, the addition of native SAP AI Core support is well-scoped and includes sensible authentication, concurrency controls, and tests. CI has passed and no exploitable vulnerabilities were identified, though a couple of hardening improvements are recommended.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 116 The code constructs token and API URLs directly from configuration without enforcing HTTPS. If misconfigured to use http://, client credentials and tokens could be transmitted in cleartext. Consider validating that AuthURL and APIURL use https and reject or warn on insecure schemes, with an explicit override only for trusted test environments.
2 [MINOR] llm/aicore.go 150 Error paths include truncated upstream response bodies (up to 200 chars) in error messages. While truncated, this may still leak sensitive upstream details into logs (e.g., error descriptions, request identifiers). Consider further redaction or logging only minimal metadata (status code, request ID) by default, with an opt-in debug flag for body snippets.

Recommendation

APPROVE — The AI Core integration is solid and security-conscious (OAuth2 client credentials flow, token caching with locks, limited error body exposure). To further harden the implementation, validate that AICore AuthURL/APIURL (and the derived deploymentUrl) use HTTPS before issuing requests, and consider reducing or gating the inclusion of upstream response bodies in error messages behind a debug flag. With these minor adjustments, the change set is safe to merge.


Review by security


Evaluated against 7f16af2b

# Security Review ## Summary Overall, the addition of native SAP AI Core support is well-scoped and includes sensible authentication, concurrency controls, and tests. CI has passed and no exploitable vulnerabilities were identified, though a couple of hardening improvements are recommended. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 116 | The code constructs token and API URLs directly from configuration without enforcing HTTPS. If misconfigured to use http://, client credentials and tokens could be transmitted in cleartext. Consider validating that AuthURL and APIURL use https and reject or warn on insecure schemes, with an explicit override only for trusted test environments. | | 2 | [MINOR] | `llm/aicore.go` | 150 | Error paths include truncated upstream response bodies (up to 200 chars) in error messages. While truncated, this may still leak sensitive upstream details into logs (e.g., error descriptions, request identifiers). Consider further redaction or logging only minimal metadata (status code, request ID) by default, with an opt-in debug flag for body snippets. | ## Recommendation **APPROVE** — The AI Core integration is solid and security-conscious (OAuth2 client credentials flow, token caching with locks, limited error body exposure). To further harden the implementation, validate that AICore AuthURL/APIURL (and the derived deploymentUrl) use HTTPS before issuing requests, and consider reducing or gating the inclusion of upstream response bodies in error messages behind a debug flag. With these minor adjustments, the change set is safe to merge. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against 7f16af2b*
llm/aicore.go Outdated
@@ -0,0 +113,4 @@
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := c.http.Do(req)
Collaborator

[MINOR] The code constructs token and API URLs directly from configuration without enforcing HTTPS. If misconfigured to use http://, client credentials and tokens could be transmitted in cleartext. Consider validating that AuthURL and APIURL use https and reject or warn on insecure schemes, with an explicit override only for trusted test environments.

**[MINOR]** The code constructs token and API URLs directly from configuration without enforcing HTTPS. If misconfigured to use http://, client credentials and tokens could be transmitted in cleartext. Consider validating that AuthURL and APIURL use https and reject or warn on insecure schemes, with an explicit override only for trusted test environments.
llm/aicore.go Outdated
@@ -0,0 +147,4 @@
// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.
// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.
// Also returns a valid token for use by the caller, avoiding redundant getToken calls.
//
Collaborator

[MINOR] Error paths include truncated upstream response bodies (up to 200 chars) in error messages. While truncated, this may still leak sensitive upstream details into logs (e.g., error descriptions, request identifiers). Consider further redaction or logging only minimal metadata (status code, request ID) by default, with an opt-in debug flag for body snippets.

**[MINOR]** Error paths include truncated upstream response bodies (up to 200 chars) in error messages. While truncated, this may still leak sensitive upstream details into logs (e.g., error descriptions, request identifiers). Consider further redaction or logging only minimal metadata (status code, request ID) by default, with an opt-in debug flag for body snippets.
rodin added the self-reviewed label 2026-05-10 15:54:57 +00:00
rodin removed their assignment 2026-05-10 16:02:48 +00:00
aweiker was assigned by rodin 2026-05-10 16:02:49 +00:00
rodin added the ready label 2026-05-10 16:33:08 +00:00
aweiker requested changes 2026-05-10 16:58:57 +00:00
aweiker left a comment
Contributor

ADDRESS ALL FEEDBACK before assigning it to me

ADDRESS ALL FEEDBACK before assigning it to me
rodin added 1 commit 2026-05-10 17:25:17 +00:00
feat: native SAP AI Core support
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 14s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 35s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m30s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 2m0s
7dab35de41
Add native SAP AI Core provider that handles OAuth token management and
deployment discovery automatically. This eliminates the need for the
external LLM proxy when running in SAP environments.

Changes:
- Add AICoreClient with OAuth token caching and deployment URL discovery
- Support both Anthropic and OpenAI models via AI Core deployments
- Update CI to use native AI Core provider
- Update action inputs to accept AI Core credentials
- Update README with AI Core configuration examples

Model names must match AI Core deployment names (e.g. anthropic--claude-4.6-sonnet, gpt-5).
rodin force-pushed feat/aicore-provider-v2 from 7f16af2b5b to 7dab35de41 2026-05-10 17:25:17 +00:00 Compare
sonnet-review-bot requested changes 2026-05-10 17:26:07 +00:00
sonnet-review-bot left a comment
First-time contributor

Sonnet Review

Summary

The PR adds a well-structured SAP AI Core native client with good test coverage and proper OAuth token caching. However, there is a clear bug: flag.Parse() is called twice in main(). Additionally, there are a few minor issues worth addressing.

Findings

# Severity File Line Finding
1 [MAJOR] cmd/review-bot/main.go 88 flag.Parse() is called twice consecutively (lines 87-88 in the diff context, visible in both the diff and the full file). The first call is the newly added one, and the second is the original. While harmless at runtime, this is clearly a copy-paste oversight that should be removed before merging. The original flag.Parse() at the end of the flag declarations should be the only one.
2 [MINOR] llm/aicore.go 162 Duplicate doc comment block: getDeploymentURL has its doc comment written twice in succession (lines 157-169 of aicore.go). The first copy should be removed.
3 [MINOR] llm/aicore.go 265 In fetchDeployments, when multiple RUNNING deployments exist for the same model name (as tested in TestAICoreClient_DeploymentFetch), the last one in iteration order wins — the map is overwritten silently. The test asserts deploy-789 (the second RUNNING gpt-5) is selected, but this is only guaranteed because JSON array order is preserved in Go's json.Unmarshal. This is fragile; a comment noting the last-write-wins behavior (or explicitly preferring the last RUNNING entry) would clarify intent.
4 [NIT] llm/aicore.go 1 The maxErrorBodyLen constant is unexported and only used within aicore.go, but truncateBody is also unexported. Both are fine as-is, but maxErrorBodyLen = 200 is a magic number without a unit comment. Consider renaming to maxErrorBodyBytes or adding // bytes in the comment for clarity.
5 [NIT] llm/client.go 206 The anthropicRequest struct reformatting (alignment change) is a cosmetic-only change unrelated to the feature. Not a problem, just worth noting it's pure noise in the diff.

Recommendation

REQUEST_CHANGES — The double flag.Parse() call is a clear bug introduced in this PR and must be fixed — it's at best confusing and was likely introduced by accidentally keeping both the newly moved call and the original. The duplicate doc comment in aicore.go is also a copy-paste artifact that should be cleaned up. Fix these two issues and the PR is ready to merge. The core implementation (OAuth token caching with double-checked locking, deployment discovery, routing by model prefix, test coverage with httptest) is solid and follows the project's conventions well.


Review by sonnet


Evaluated against 7dab35de

# Sonnet Review ## Summary The PR adds a well-structured SAP AI Core native client with good test coverage and proper OAuth token caching. However, there is a clear bug: `flag.Parse()` is called twice in `main()`. Additionally, there are a few minor issues worth addressing. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MAJOR] | `cmd/review-bot/main.go` | 88 | `flag.Parse()` is called twice consecutively (lines 87-88 in the diff context, visible in both the diff and the full file). The first call is the newly added one, and the second is the original. While harmless at runtime, this is clearly a copy-paste oversight that should be removed before merging. The original `flag.Parse()` at the end of the flag declarations should be the only one. | | 2 | [MINOR] | `llm/aicore.go` | 162 | Duplicate doc comment block: `getDeploymentURL` has its doc comment written twice in succession (lines 157-169 of aicore.go). The first copy should be removed. | | 3 | [MINOR] | `llm/aicore.go` | 265 | In `fetchDeployments`, when multiple RUNNING deployments exist for the same model name (as tested in `TestAICoreClient_DeploymentFetch`), the last one in iteration order wins — the map is overwritten silently. The test asserts `deploy-789` (the second RUNNING gpt-5) is selected, but this is only guaranteed because JSON array order is preserved in Go's `json.Unmarshal`. This is fragile; a comment noting the last-write-wins behavior (or explicitly preferring the last RUNNING entry) would clarify intent. | | 4 | [NIT] | `llm/aicore.go` | 1 | The `maxErrorBodyLen` constant is unexported and only used within `aicore.go`, but `truncateBody` is also unexported. Both are fine as-is, but `maxErrorBodyLen = 200` is a magic number without a unit comment. Consider renaming to `maxErrorBodyBytes` or adding `// bytes` in the comment for clarity. | | 5 | [NIT] | `llm/client.go` | 206 | The `anthropicRequest` struct reformatting (alignment change) is a cosmetic-only change unrelated to the feature. Not a problem, just worth noting it's pure noise in the diff. | ## Recommendation **REQUEST_CHANGES** — The double `flag.Parse()` call is a clear bug introduced in this PR and must be fixed — it's at best confusing and was likely introduced by accidentally keeping both the newly moved call and the original. The duplicate doc comment in `aicore.go` is also a copy-paste artifact that should be cleaned up. Fix these two issues and the PR is ready to merge. The core implementation (OAuth token caching with double-checked locking, deployment discovery, routing by model prefix, test coverage with httptest) is solid and follows the project's conventions well. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 7dab35de*
@@ -0,0 +1,391 @@
package llm
First-time contributor

[NIT] The maxErrorBodyLen constant is unexported and only used within aicore.go, but truncateBody is also unexported. Both are fine as-is, but maxErrorBodyLen = 200 is a magic number without a unit comment. Consider renaming to maxErrorBodyBytes or adding // bytes in the comment for clarity.

**[NIT]** The `maxErrorBodyLen` constant is unexported and only used within `aicore.go`, but `truncateBody` is also unexported. Both are fine as-is, but `maxErrorBodyLen = 200` is a magic number without a unit comment. Consider renaming to `maxErrorBodyBytes` or adding `// bytes` in the comment for clarity.
@@ -0,0 +159,4 @@
// Still need a token for the caller
token, err = c.getToken(ctx)
if err != nil {
return "", "", fmt.Errorf("get token: %w", err)
First-time contributor

[MINOR] Duplicate doc comment block: getDeploymentURL has its doc comment written twice in succession (lines 157-169 of aicore.go). The first copy should be removed.

**[MINOR]** Duplicate doc comment block: `getDeploymentURL` has its doc comment written twice in succession (lines 157-169 of aicore.go). The first copy should be removed.
@@ -0,0 +262,4 @@
} else {
userMessages = append(userMessages, anthropicMsg{
Role: m.Role,
Content: m.Content,
First-time contributor

[MINOR] In fetchDeployments, when multiple RUNNING deployments exist for the same model name (as tested in TestAICoreClient_DeploymentFetch), the last one in iteration order wins — the map is overwritten silently. The test asserts deploy-789 (the second RUNNING gpt-5) is selected, but this is only guaranteed because JSON array order is preserved in Go's json.Unmarshal. This is fragile; a comment noting the last-write-wins behavior (or explicitly preferring the last RUNNING entry) would clarify intent.

**[MINOR]** In `fetchDeployments`, when multiple RUNNING deployments exist for the same model name (as tested in `TestAICoreClient_DeploymentFetch`), the last one in iteration order wins — the map is overwritten silently. The test asserts `deploy-789` (the second RUNNING gpt-5) is selected, but this is only guaranteed because JSON array order is preserved in Go's `json.Unmarshal`. This is fragile; a comment noting the last-write-wins behavior (or explicitly preferring the last RUNNING entry) would clarify intent.
@@ -176,11 +206,12 @@ func (c *Client) completeOpenAI(ctx context.Context, messages []Message) (string
// --- Anthropic Messages API implementation ---
First-time contributor

[NIT] The anthropicRequest struct reformatting (alignment change) is a cosmetic-only change unrelated to the feature. Not a problem, just worth noting it's pure noise in the diff.

**[NIT]** The `anthropicRequest` struct reformatting (alignment change) is a cosmetic-only change unrelated to the feature. Not a problem, just worth noting it's pure noise in the diff.
gpt-review-bot approved these changes 2026-05-10 17:27:03 +00:00
gpt-review-bot left a comment
First-time contributor

Gpt Review

Summary

Solid addition of native SAP AI Core support with clear configuration, proper OAuth handling, deployment discovery, and comprehensive tests. The implementation follows idiomatic Go patterns and integrates cleanly with the existing provider abstraction. Only minor nits remain.

Findings

# Severity File Line Finding
1 [MINOR] cmd/review-bot/main.go 79 flag.Parse() is invoked twice; the extra call is redundant and can be removed to avoid confusion.
2 [NIT] llm/aicore.go 150 Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.") — remove the repeated sentence.

Recommendation

APPROVE — The changes are well-designed and tested, and CI has passed. Approve the PR. As follow-ups, remove the redundant second flag.Parse() call in cmd/review-bot/main.go and clean up the duplicated comment in llm/aicore.go for clarity. Otherwise, the AI Core integration, error handling, concurrency, and testing look strong.


Review by gpt


Evaluated against 7dab35de

# Gpt Review ## Summary Solid addition of native SAP AI Core support with clear configuration, proper OAuth handling, deployment discovery, and comprehensive tests. The implementation follows idiomatic Go patterns and integrates cleanly with the existing provider abstraction. Only minor nits remain. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `cmd/review-bot/main.go` | 79 | flag.Parse() is invoked twice; the extra call is redundant and can be removed to avoid confusion. | | 2 | [NIT] | `llm/aicore.go` | 150 | Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.") — remove the repeated sentence. | ## Recommendation **APPROVE** — The changes are well-designed and tested, and CI has passed. Approve the PR. As follow-ups, remove the redundant second flag.Parse() call in cmd/review-bot/main.go and clean up the duplicated comment in llm/aicore.go for clarity. Otherwise, the AI Core integration, error handling, concurrency, and testing look strong. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 7dab35de*
@@ -75,0 +76,4 @@
aicoreClientID := flag.String("aicore-client-id", envOrDefault("AICORE_CLIENT_ID", ""), "SAP AI Core client ID (for provider=aicore)")
aicoreClientSecret := flag.String("aicore-client-secret", envOrDefault("AICORE_CLIENT_SECRET", ""), "SAP AI Core client secret (for provider=aicore)")
aicoreAuthURL := flag.String("aicore-auth-url", envOrDefault("AICORE_AUTH_URL", ""), "SAP AI Core auth URL (for provider=aicore)")
aicoreAPIURL := flag.String("aicore-api-url", envOrDefault("AICORE_API_URL", ""), "SAP AI Core API URL (for provider=aicore)")
First-time contributor

[MINOR] flag.Parse() is invoked twice; the extra call is redundant and can be removed to avoid confusion.

**[MINOR]** flag.Parse() is invoked twice; the extra call is redundant and can be removed to avoid confusion.
@@ -0,0 +147,4 @@
// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.
// getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.
// Also returns a valid token for use by the caller, avoiding redundant getToken calls.
//
First-time contributor

[NIT] Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.") — remove the repeated sentence.

**[NIT]** Duplicate comment line above getDeploymentURL ("getDeploymentURL returns the deployment URL for a model, fetching deployments if needed.") — remove the repeated sentence.
rodin removed the readyself-reviewed labels 2026-05-10 17:27:19 +00:00
aweiker was unassigned by rodin 2026-05-10 17:27:19 +00:00
rodin self-assigned this 2026-05-10 17:27:19 +00:00
security-review-bot requested review from security-review-bot 2026-05-10 17:27:33 +00:00
security-review-bot approved these changes 2026-05-10 17:27:33 +00:00
security-review-bot left a comment
Collaborator

Security Review

Summary

Native SAP AI Core support is implemented with OAuth2, deployment discovery, and provider wiring. The changes avoid logging secrets and use bounded error body inclusion; no exploitable vulnerabilities were identified. A few hardening opportunities exist around URL validation and trusting deployment URLs.

Findings

# Severity File Line Finding
1 [MINOR] llm/aicore.go 96 Auth and API endpoints are accepted as-is without scheme validation. If misconfigured to use http://, client_id/client_secret and bearer tokens would be transmitted in cleartext. Enforce HTTPS (reject non-https schemes) for AICORE_AUTH_URL and AICORE_API_URL to prevent credential leakage.
2 [MINOR] llm/aicore.go 198 The code caches and later calls the absolute deploymentUrl returned by the AI Core API and sends the OAuth bearer token to that URL. If the API (or DNS) is compromised or misconfigured, this could direct requests (with tokens) to an unexpected host. Consider validating that deploymentUrl uses https and matches the expected AI Core host (or is under the configured API domain) before use.
3 [NIT] llm/aicore.go 115 Error messages include upstream response bodies (truncated to 200 bytes) for token and API failures. While truncation reduces risk, upstream bodies can still contain sensitive diagnostic information. Consider omitting bodies by default or only including sanitized snippets for specific, known-safe error formats.

Recommendation

APPROVE — Overall the implementation appears sound and secure-by-default in most aspects: credentials are not logged, OAuth tokens are cached with a buffer, and error bodies are truncated. CI passes. To further harden the AI Core client, enforce https schemes for AICORE_AUTH_URL and AICORE_API_URL to avoid accidental plaintext credential transmission, and validate that deployment URLs returned from the API are https and belong to the expected host before using them with the bearer token. Optionally reduce or gate inclusion of upstream error bodies in logs. With these minor improvements, the provider integration will be more robust against misconfiguration and supply-chain issues.


Review by security


Evaluated against 7dab35de

# Security Review ## Summary Native SAP AI Core support is implemented with OAuth2, deployment discovery, and provider wiring. The changes avoid logging secrets and use bounded error body inclusion; no exploitable vulnerabilities were identified. A few hardening opportunities exist around URL validation and trusting deployment URLs. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `llm/aicore.go` | 96 | Auth and API endpoints are accepted as-is without scheme validation. If misconfigured to use http://, client_id/client_secret and bearer tokens would be transmitted in cleartext. Enforce HTTPS (reject non-https schemes) for AICORE_AUTH_URL and AICORE_API_URL to prevent credential leakage. | | 2 | [MINOR] | `llm/aicore.go` | 198 | The code caches and later calls the absolute deploymentUrl returned by the AI Core API and sends the OAuth bearer token to that URL. If the API (or DNS) is compromised or misconfigured, this could direct requests (with tokens) to an unexpected host. Consider validating that deploymentUrl uses https and matches the expected AI Core host (or is under the configured API domain) before use. | | 3 | [NIT] | `llm/aicore.go` | 115 | Error messages include upstream response bodies (truncated to 200 bytes) for token and API failures. While truncation reduces risk, upstream bodies can still contain sensitive diagnostic information. Consider omitting bodies by default or only including sanitized snippets for specific, known-safe error formats. | ## Recommendation **APPROVE** — Overall the implementation appears sound and secure-by-default in most aspects: credentials are not logged, OAuth tokens are cached with a buffer, and error bodies are truncated. CI passes. To further harden the AI Core client, enforce https schemes for AICORE_AUTH_URL and AICORE_API_URL to avoid accidental plaintext credential transmission, and validate that deployment URLs returned from the API are https and belong to the expected host before using them with the bearer token. Optionally reduce or gate inclusion of upstream error bodies in logs. With these minor improvements, the provider integration will be more robust against misconfiguration and supply-chain issues. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against 7dab35de*
@@ -0,0 +93,4 @@
token, expiry, err := c.fetchToken(ctx)
if err != nil {
return "", err
}
Collaborator

[MINOR] Auth and API endpoints are accepted as-is without scheme validation. If misconfigured to use http://, client_id/client_secret and bearer tokens would be transmitted in cleartext. Enforce HTTPS (reject non-https schemes) for AICORE_AUTH_URL and AICORE_API_URL to prevent credential leakage.

**[MINOR]** Auth and API endpoints are accepted as-is without scheme validation. If misconfigured to use http://, client_id/client_secret and bearer tokens would be transmitted in cleartext. Enforce HTTPS (reject non-https schemes) for AICORE_AUTH_URL and AICORE_API_URL to prevent credential leakage.
@@ -0,0 +112,4 @@
return "", time.Time{}, fmt.Errorf("create token request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
Collaborator

[NIT] Error messages include upstream response bodies (truncated to 200 bytes) for token and API failures. While truncation reduces risk, upstream bodies can still contain sensitive diagnostic information. Consider omitting bodies by default or only including sanitized snippets for specific, known-safe error formats.

**[NIT]** Error messages include upstream response bodies (truncated to 200 bytes) for token and API failures. While truncation reduces risk, upstream bodies can still contain sensitive diagnostic information. Consider omitting bodies by default or only including sanitized snippets for specific, known-safe error formats.
@@ -0,0 +195,4 @@
if err != nil {
return fmt.Errorf("create deployments request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
Collaborator

[MINOR] The code caches and later calls the absolute deploymentUrl returned by the AI Core API and sends the OAuth bearer token to that URL. If the API (or DNS) is compromised or misconfigured, this could direct requests (with tokens) to an unexpected host. Consider validating that deploymentUrl uses https and matches the expected AI Core host (or is under the configured API domain) before use.

**[MINOR]** The code caches and later calls the absolute deploymentUrl returned by the AI Core API and sends the OAuth bearer token to that URL. If the API (or DNS) is compromised or misconfigured, this could direct requests (with tokens) to an unexpected host. Consider validating that deploymentUrl uses https and matches the expected AI Core host (or is under the configured API domain) before use.
rodin added the self-reviewed label 2026-05-10 17:34:43 +00:00
rodin removed their assignment 2026-05-10 17:34:51 +00:00
aweiker was assigned by rodin 2026-05-10 17:34:51 +00:00
rodin added the ready label 2026-05-10 17:34:55 +00:00
rodin removed the readyself-reviewed labels 2026-05-10 17:52:39 +00:00
aweiker was unassigned by rodin 2026-05-10 17:52:39 +00:00
rodin self-assigned this 2026-05-10 17:52:39 +00:00
rodin added the ready label 2026-05-10 17:57:59 +00:00
rodin removed their assignment 2026-05-10 17:57:59 +00:00
aweiker was assigned by rodin 2026-05-10 17:57:59 +00:00
rodin removed the ready label 2026-05-10 18:00:17 +00:00
aweiker was unassigned by rodin 2026-05-10 18:00:17 +00:00
rodin self-assigned this 2026-05-10 18:00:17 +00:00
rodin added the self-reviewed label 2026-05-10 18:11:58 +00:00
rodin removed their assignment 2026-05-10 18:12:06 +00:00
aweiker was assigned by rodin 2026-05-10 18:12:06 +00:00
rodin removed the self-reviewed label 2026-05-10 18:13:09 +00:00
aweiker was unassigned by rodin 2026-05-10 18:13:09 +00:00
rodin self-assigned this 2026-05-10 18:13:09 +00:00
rodin merged commit 230419f0e2 into main 2026-05-10 20:03:32 +00:00
Sign in to join this conversation.
5 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rodin/review-bot#54