backend/kiro: port fixes from goq amzcodewhisp
- Switch endpoint from q.*.amazonaws.com to runtime.*.kiro.dev - Use management.*.kiro.dev for ListModels - Fix content encoding: plain string instead of json.RawMessage - Fix tool results: always use text field (upstream rejects bare JSON primitives) - Normalize tool call arguments (deduplicate keys) - Expand retry conditions: add 502, 503, 408, InsufficientModelCapacity - Add SQLite busy-timeout and Go-level retry for lock contention - Add 30s refresh timeout and fullLoad before refresh - Add SSO cache fallback for token refresh - Add jitter to token expiry checks - Add sqliteHasNewUsableAccessToken for cross-process recovery - Add envState to current message - Update user-agent strings to 1.3.15/2.5.1 - Set X-Amzn-Codewhisperer-Optout to true - Add reasoning/thinking support (additionalModelRequestFields) - Add counter-thinking tags on user messages before assistants - Handle reasoningContentEvent in stream
This commit is contained in:
parent
0fc9c9613c
commit
75f137dda5
|
|
@ -16,6 +16,7 @@ package backend
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -133,7 +134,7 @@ func (b *CodeWhispererBackend) ChatStream(
|
|||
return nil, fmt.Errorf("kiro profile ARN: %w", err)
|
||||
}
|
||||
|
||||
req, err := buildKiroRequest(model, messages, tools, profileARN)
|
||||
req, err := buildKiroRequest(model, messages, tools, profileARN, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -181,6 +182,16 @@ func (b *CodeWhispererBackend) runStream(ctx context.Context, req *kiroGenerateR
|
|||
stopped = true
|
||||
}
|
||||
},
|
||||
OnReasoningDelta: func(event kiroReasoningContentEvent) {
|
||||
if stopped || (event.Text == "" && event.RedactedContent == "" && event.Signature == "") {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case ch <- StreamEvent{Reasoning: event.Text}:
|
||||
case <-ctx.Done():
|
||||
stopped = true
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
if streamErr != nil {
|
||||
|
|
@ -255,12 +266,15 @@ func kiroSanitizeModel(model string) string {
|
|||
return model
|
||||
}
|
||||
|
||||
func buildKiroRequest(model string, messages []Message, tools []Tool, profileARN string) (*kiroGenerateRequest, error) {
|
||||
func buildKiroRequest(model string, messages []Message, tools []Tool, profileARN string, params GenerationParams) (*kiroGenerateRequest, error) {
|
||||
model = kiroSanitizeModel(model)
|
||||
if len(messages) == 0 {
|
||||
return nil, fmt.Errorf("at least one message is required")
|
||||
}
|
||||
|
||||
// Apply counter-thinking tags to user messages before assistant responses.
|
||||
messages = kiroApplyCounterThinkingTags(messages)
|
||||
|
||||
encodedTools := encodeKiroToolDefinitions(tools)
|
||||
|
||||
lastRole := strings.ToLower(strings.TrimSpace(messages[len(messages)-1].Role))
|
||||
|
|
@ -275,7 +289,7 @@ func buildKiroRequest(model string, messages []Message, tools []Tool, profileARN
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
current = kiroChatMessage{UserInputMessage: encodeKiroUserMessage(messages[len(messages)-1], model, encodedTools, nil)}
|
||||
current = kiroChatMessage{UserInputMessage: encodeKiroUserMessage(messages[len(messages)-1], model, encodedTools, nil, true)}
|
||||
|
||||
case "tool":
|
||||
// Find the last non-tool message.
|
||||
|
|
@ -288,7 +302,7 @@ func buildKiroRequest(model string, messages []Message, tools []Tool, profileARN
|
|||
return nil, err
|
||||
}
|
||||
toolResults, images := encodeKiroToolResults(messages[cutoff+1:])
|
||||
current = kiroChatMessage{UserInputMessage: emptyKiroUserMessage(model, encodedTools, toolResults, images)}
|
||||
current = kiroChatMessage{UserInputMessage: emptyKiroUserMessage(model, encodedTools, toolResults, images, true)}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("last message role %q is not supported (expected user, system, or tool)", lastRole)
|
||||
|
|
@ -303,7 +317,7 @@ func buildKiroRequest(model string, messages []Message, tools []Tool, profileARN
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return &kiroGenerateRequest{
|
||||
req := &kiroGenerateRequest{
|
||||
ProfileARN: profileARN,
|
||||
ConversationState: kiroConversationState{
|
||||
ConversationID: convID,
|
||||
|
|
@ -313,7 +327,14 @@ func buildKiroRequest(model string, messages []Message, tools []Tool, profileARN
|
|||
AgentContinuationID: contID,
|
||||
AgentTaskType: kiroAgentTaskType,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Set additionalModelRequestFields for reasoning/thinking support.
|
||||
if fields := kiroModelRequestFieldsFromParams(params); fields != nil {
|
||||
req.AdditionalModelRequestFields = fields
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeKiroHistory(messages []Message) ([]kiroChatMessage, error) {
|
||||
|
|
@ -322,7 +343,7 @@ func encodeKiroHistory(messages []Message) ([]kiroChatMessage, error) {
|
|||
m := messages[i]
|
||||
switch strings.ToLower(strings.TrimSpace(m.Role)) {
|
||||
case "user", "system":
|
||||
out = append(out, kiroChatMessage{UserInputMessage: encodeKiroUserMessage(m, "", nil, nil)})
|
||||
out = append(out, kiroChatMessage{UserInputMessage: encodeKiroUserMessage(m, "", nil, nil, false)})
|
||||
case "assistant":
|
||||
out = append(out, kiroChatMessage{AssistantResponseMessage: encodeKiroAssistantMessage(m)})
|
||||
case "tool":
|
||||
|
|
@ -332,17 +353,18 @@ func encodeKiroHistory(messages []Message) ([]kiroChatMessage, error) {
|
|||
i++
|
||||
}
|
||||
results, imgs := encodeKiroToolResults(messages[start : i+1])
|
||||
out = append(out, kiroChatMessage{UserInputMessage: emptyKiroUserMessage("", nil, results, imgs)})
|
||||
out = append(out, kiroChatMessage{UserInputMessage: emptyKiroUserMessage("", nil, results, imgs, false)})
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func encodeKiroUserMessage(m Message, modelID string, tools []kiroTool, toolResults []kiroToolResult) *kiroUserInputMessage {
|
||||
func encodeKiroUserMessage(m Message, modelID string, tools []kiroTool, toolResults []kiroToolResult, includeEnvState bool) *kiroUserInputMessage {
|
||||
msg := &kiroUserInputMessage{
|
||||
Content: kiroEncodeContent(m),
|
||||
Content: kiroExtractTextContent(m),
|
||||
Origin: kiroDefaultOrigin,
|
||||
ModelID: modelID,
|
||||
Images: kiroExtractImages(m),
|
||||
}
|
||||
ctx := &kiroUserInputContext{}
|
||||
if len(tools) > 0 {
|
||||
|
|
@ -351,15 +373,18 @@ func encodeKiroUserMessage(m Message, modelID string, tools []kiroTool, toolResu
|
|||
if len(toolResults) > 0 {
|
||||
ctx.ToolResults = toolResults
|
||||
}
|
||||
if len(ctx.Tools) > 0 || len(ctx.ToolResults) > 0 {
|
||||
if includeEnvState {
|
||||
ctx.EnvState = kiroCurrentEnvState()
|
||||
}
|
||||
if len(ctx.Tools) > 0 || len(ctx.ToolResults) > 0 || ctx.EnvState != nil {
|
||||
msg.UserInputMessageContext = ctx
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func emptyKiroUserMessage(modelID string, tools []kiroTool, toolResults []kiroToolResult, images []kiroImageBlock) *kiroUserInputMessage {
|
||||
func emptyKiroUserMessage(modelID string, tools []kiroTool, toolResults []kiroToolResult, images []kiroImageBlock, includeEnvState bool) *kiroUserInputMessage {
|
||||
msg := &kiroUserInputMessage{
|
||||
Content: kiroEncodeStringContent(""),
|
||||
Content: "",
|
||||
Origin: kiroDefaultOrigin,
|
||||
ModelID: modelID,
|
||||
Images: images,
|
||||
|
|
@ -371,7 +396,10 @@ func emptyKiroUserMessage(modelID string, tools []kiroTool, toolResults []kiroTo
|
|||
if len(toolResults) > 0 {
|
||||
ctx.ToolResults = toolResults
|
||||
}
|
||||
if len(ctx.Tools) > 0 || len(ctx.ToolResults) > 0 {
|
||||
if includeEnvState {
|
||||
ctx.EnvState = kiroCurrentEnvState()
|
||||
}
|
||||
if len(ctx.Tools) > 0 || len(ctx.ToolResults) > 0 || ctx.EnvState != nil {
|
||||
msg.UserInputMessageContext = ctx
|
||||
}
|
||||
return msg
|
||||
|
|
@ -383,6 +411,16 @@ func encodeKiroAssistantMessage(m Message) *kiroAssistantResponseMessage {
|
|||
input := tc.Arguments
|
||||
if len(input) == 0 || !json.Valid(input) {
|
||||
input = json.RawMessage("{}")
|
||||
} else {
|
||||
// Normalize the JSON by round-tripping through unmarshal/marshal.
|
||||
// This deduplicates keys (the model sometimes produces duplicate
|
||||
// keys which the upstream API rejects) and ensures well-formed output.
|
||||
var parsed any
|
||||
if err := json.Unmarshal(input, &parsed); err == nil {
|
||||
if normalized, err := json.Marshal(parsed); err == nil {
|
||||
input = normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
toolUseID := strings.TrimSpace(tc.ID)
|
||||
if toolUseID == "" {
|
||||
|
|
@ -464,63 +502,136 @@ func encodeKiroToolResultBlocks(m Message) ([]kiroToolResultContent, []kiroImage
|
|||
}
|
||||
|
||||
func encodeKiroToolResultContent(content string) kiroToolResultContent {
|
||||
content = strings.TrimSpace(content)
|
||||
if len(content) > 0 && content[0] == '{' && json.Valid([]byte(content)) {
|
||||
return kiroToolResultContent{JSON: json.RawMessage(content)}
|
||||
}
|
||||
return kiroToolResultContent{Text: content}
|
||||
// Always use the text field. The upstream Amazon Q API rejects bare JSON
|
||||
// primitives (numbers, strings, booleans, null) in the "json" field with
|
||||
// "ValidationException: Improperly formed request." — it expects only
|
||||
// objects or arrays. Using text is always safe; the model sees the same
|
||||
// content either way.
|
||||
return kiroToolResultContent{Text: strings.TrimSpace(content)}
|
||||
}
|
||||
|
||||
// kiroEncodeContent returns a json.RawMessage that is either a JSON string
|
||||
// (plain text) or a JSON array of Bedrock Converse content blocks (when images
|
||||
// are present).
|
||||
func kiroEncodeContent(m Message) json.RawMessage {
|
||||
// kiroExtractTextContent returns the text content of a message. When images are
|
||||
// present, they are encoded as separate image blocks on the message; this
|
||||
// function only returns the text portion.
|
||||
func kiroExtractTextContent(m Message) string {
|
||||
if len(m.ContentBlocks) == 0 {
|
||||
return kiroEncodeStringContent(m.Content)
|
||||
return m.Content
|
||||
}
|
||||
hasImage := false
|
||||
var sb strings.Builder
|
||||
for _, cb := range m.ContentBlocks {
|
||||
if cb.Type == "text" {
|
||||
sb.WriteString(cb.Text)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// kiroExtractImages extracts image blocks from a message's ContentBlocks.
|
||||
func kiroExtractImages(m Message) []kiroImageBlock {
|
||||
if len(m.ContentBlocks) == 0 {
|
||||
return nil
|
||||
}
|
||||
var images []kiroImageBlock
|
||||
for _, cb := range m.ContentBlocks {
|
||||
if cb.Type == "image" && cb.ImageSource != nil {
|
||||
hasImage = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasImage {
|
||||
// Text-only blocks: concatenate and send as string.
|
||||
var sb strings.Builder
|
||||
for _, cb := range m.ContentBlocks {
|
||||
if cb.Type == "text" {
|
||||
sb.WriteString(cb.Text)
|
||||
}
|
||||
}
|
||||
return kiroEncodeStringContent(sb.String())
|
||||
}
|
||||
// Mixed content: encode as Bedrock Converse content block array.
|
||||
blocks := make([]kiroContentBlock, 0, len(m.ContentBlocks))
|
||||
for _, cb := range m.ContentBlocks {
|
||||
switch cb.Type {
|
||||
case "text":
|
||||
blocks = append(blocks, kiroContentBlock{Text: cb.Text})
|
||||
case "image":
|
||||
if cb.ImageSource == nil {
|
||||
continue
|
||||
}
|
||||
format := strings.TrimPrefix(cb.ImageSource.MediaType, "image/")
|
||||
blocks = append(blocks, kiroContentBlock{
|
||||
Image: &kiroImageBlock{
|
||||
Format: format,
|
||||
Source: kiroImageSource{Bytes: cb.ImageSource.Data},
|
||||
},
|
||||
images = append(images, kiroImageBlock{
|
||||
Format: format,
|
||||
Source: kiroImageSource{Bytes: cb.ImageSource.Data},
|
||||
})
|
||||
}
|
||||
}
|
||||
data, _ := json.Marshal(blocks)
|
||||
return json.RawMessage(data)
|
||||
return images
|
||||
}
|
||||
|
||||
func kiroEncodeStringContent(s string) json.RawMessage {
|
||||
data, _ := json.Marshal(s)
|
||||
return json.RawMessage(data)
|
||||
func kiroCurrentEnvState() *kiroEnvState {
|
||||
return &kiroEnvState{
|
||||
OperatingSystem: runtime.GOOS,
|
||||
CurrentWorkingDirectory: "/",
|
||||
}
|
||||
}
|
||||
|
||||
// ── Counter-thinking & reasoning support ──────────────────────────────────────
|
||||
|
||||
const kiroCounterThinkingInstructions = `Be thorough in your work; don't skip steps or cut corners to save time. Think deeply and step by step, do not skip ahead prematurely.`
|
||||
const kiroDefaultReasoningEffort = "95"
|
||||
|
||||
// kiroApplyCounterThinkingTags appends corrective tags to user messages that
|
||||
// immediately precede assistant responses, countering server-side injections
|
||||
// that degrade reasoning quality.
|
||||
func kiroApplyCounterThinkingTags(messages []Message) []Message {
|
||||
if len(messages) < 2 {
|
||||
return messages
|
||||
}
|
||||
var mangled []Message
|
||||
for i := 1; i < len(messages); i++ {
|
||||
if !strings.EqualFold(messages[i].Role, "assistant") {
|
||||
continue
|
||||
}
|
||||
prev := messages[i-1]
|
||||
role := strings.ToLower(strings.TrimSpace(prev.Role))
|
||||
if role != "user" && role != "system" {
|
||||
continue
|
||||
}
|
||||
if mangled == nil {
|
||||
mangled = make([]Message, len(messages))
|
||||
copy(mangled, messages)
|
||||
}
|
||||
suffix := "\n\n<reasoning_effort>" + kiroDefaultReasoningEffort + "</reasoning_effort>\n" +
|
||||
"<reasoning_effort>" + kiroDefaultReasoningEffort + "</reasoning_effort>\n" +
|
||||
"<implicitInstruction>" + kiroCounterThinkingInstructions + "</implicitInstruction>"
|
||||
mangled[i-1].Content = prev.Content + suffix
|
||||
}
|
||||
if mangled != nil {
|
||||
return mangled
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
// kiroModelRequestFieldsFromParams builds additionalModelRequestFields from
|
||||
// GenerationParams. Returns nil if no reasoning/thinking fields are needed.
|
||||
func kiroModelRequestFieldsFromParams(params GenerationParams) *kiroModelRequestFields {
|
||||
fields := &kiroModelRequestFields{}
|
||||
hasFields := false
|
||||
|
||||
// Enable summarized thinking by default for reasoning-capable models.
|
||||
if params.ThinkingBudget > 0 || params.ReasoningEffort != "" {
|
||||
fields.Thinking = &kiroThinkingConfig{
|
||||
Type: "adaptive",
|
||||
Display: "summarized",
|
||||
}
|
||||
hasFields = true
|
||||
}
|
||||
|
||||
// Map reasoning effort.
|
||||
if effort := kiroMapEffort(params.ReasoningEffort); effort != "" {
|
||||
fields.OutputConfig = &kiroOutputConfig{Effort: effort}
|
||||
hasFields = true
|
||||
} else if params.ThinkingBudget > 0 {
|
||||
// Default to high effort when thinking is requested but no specific effort.
|
||||
fields.OutputConfig = &kiroOutputConfig{Effort: "high"}
|
||||
hasFields = true
|
||||
}
|
||||
|
||||
if !hasFields {
|
||||
return nil
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func kiroMapEffort(effort string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(effort)) {
|
||||
case "max":
|
||||
return "max"
|
||||
case "high":
|
||||
return "high"
|
||||
case "medium":
|
||||
return "medium"
|
||||
case "low", "minimal":
|
||||
return "low"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ── Auth sources ──────────────────────────────────────────────────────────────
|
||||
|
|
@ -565,6 +676,10 @@ type sqliteKiroAuth struct {
|
|||
loaded bool
|
||||
initMu sync.Mutex
|
||||
mu sync.Mutex
|
||||
// jitter is computed once at creation time so all checks within this
|
||||
// process use the same refresh threshold. Different processes get
|
||||
// different jitter values, spreading refresh attempts over time.
|
||||
jitter time.Duration
|
||||
}
|
||||
|
||||
type kiroSQLiteState struct {
|
||||
|
|
@ -609,7 +724,7 @@ func (s *sqliteKiroAuth) AccessToken(ctx context.Context) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if state.AccessToken != "" && !kiroTokenExpiringSoon(state.ExpiresAt) {
|
||||
if state.AccessToken != "" && !s.tokenExpiringSoon(state.ExpiresAt) {
|
||||
return state.AccessToken, nil
|
||||
}
|
||||
if !s.canRefreshState(state) {
|
||||
|
|
@ -635,7 +750,14 @@ func (s *sqliteKiroAuth) Refresh(ctx context.Context) error {
|
|||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
state, err := s.load(ctx)
|
||||
// Bound the refresh operation to avoid hanging indefinitely.
|
||||
refreshCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Always perform a full reload of all sqlite state (including device
|
||||
// registration) before refreshing. Another process (e.g. kiro-cli login)
|
||||
// may have rotated the client credentials since our initial load.
|
||||
state, err := s.fullLoad(refreshCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -644,8 +766,8 @@ func (s *sqliteKiroAuth) Refresh(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// Re-read current token — another process may have already refreshed it.
|
||||
currentAccess, currentRefresh, currentExpiry, err := kiroReadSQLiteTokens(ctx, s.path)
|
||||
if err == nil && currentAccess != "" && currentAccess != state.AccessToken && !kiroTokenExpiringSoon(currentExpiry) {
|
||||
currentAccess, currentRefresh, currentExpiry, err := kiroReadSQLiteTokens(refreshCtx, s.path)
|
||||
if err == nil && currentAccess != "" && currentAccess != state.AccessToken && !s.tokenExpiringSoon(currentExpiry) {
|
||||
return nil // already refreshed by another process
|
||||
}
|
||||
|
||||
|
|
@ -655,12 +777,12 @@ func (s *sqliteKiroAuth) Refresh(ctx context.Context) error {
|
|||
}
|
||||
|
||||
if state.IsSocial {
|
||||
return s.refreshSocial(ctx, state, refreshToken)
|
||||
return s.refreshSocial(refreshCtx, state, refreshToken)
|
||||
}
|
||||
return s.refreshOIDC(ctx, state, refreshToken)
|
||||
return s.refreshOIDC(refreshCtx, state, refreshToken, currentAccess)
|
||||
}
|
||||
|
||||
func (s *sqliteKiroAuth) refreshOIDC(ctx context.Context, state kiroSQLiteState, refreshToken string) error {
|
||||
func (s *sqliteKiroAuth) refreshOIDC(ctx context.Context, state kiroSQLiteState, refreshToken, currentAccess string) error {
|
||||
oidcEndpoint := fmt.Sprintf("https://oidc.%s.amazonaws.com/token", strings.TrimSpace(state.Region))
|
||||
client := newKiroOIDCClient(oidcEndpoint, nil)
|
||||
|
||||
|
|
@ -671,6 +793,14 @@ func (s *sqliteKiroAuth) refreshOIDC(ctx context.Context, state kiroSQLiteState,
|
|||
RefreshToken: refreshToken,
|
||||
})
|
||||
if err != nil {
|
||||
// Try SSO cache fallback.
|
||||
if fallbackErr := s.refreshFromSSOCache(ctx, currentAccess); fallbackErr == nil {
|
||||
return nil
|
||||
}
|
||||
// Check if another process refreshed while we were trying.
|
||||
if s.sqliteHasNewUsableAccessToken(ctx, currentAccess) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -680,7 +810,13 @@ func (s *sqliteKiroAuth) refreshOIDC(ctx context.Context, state kiroSQLiteState,
|
|||
if resp.ExpiresIn != nil && *resp.ExpiresIn > 0 {
|
||||
expiresAt = time.Now().Add(time.Duration(*resp.ExpiresIn) * time.Second).UTC().Format(time.RFC3339)
|
||||
}
|
||||
return kiroUpdateSQLiteTokens(ctx, s.path, newAccess, newRefresh, expiresAt)
|
||||
if err := kiroUpdateSQLiteTokens(ctx, s.path, newAccess, newRefresh, expiresAt); err != nil {
|
||||
if s.sqliteHasNewUsableAccessToken(ctx, currentAccess) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sqliteKiroAuth) refreshSocial(ctx context.Context, state kiroSQLiteState, refreshToken string) error {
|
||||
|
|
@ -747,8 +883,117 @@ func (s *sqliteKiroAuth) load(ctx context.Context) (kiroSQLiteState, error) {
|
|||
return state, nil
|
||||
}
|
||||
|
||||
// fullLoad performs a complete reload of all sqlite state including device
|
||||
// registration (ClientID, ClientSecret). Used in Refresh() to pick up
|
||||
// credentials rotated by another process (e.g. kiro-cli login).
|
||||
func (s *sqliteKiroAuth) fullLoad(ctx context.Context) (kiroSQLiteState, error) {
|
||||
state, err := kiroLoadSQLiteState(ctx, s.path)
|
||||
if err != nil {
|
||||
return kiroSQLiteState{}, err
|
||||
}
|
||||
s.initMu.Lock()
|
||||
s.state = state
|
||||
s.loaded = true
|
||||
s.initMu.Unlock()
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// tokenExpiringSoon checks if the token is about to expire, using instance
|
||||
// jitter to spread refresh attempts across processes.
|
||||
func (s *sqliteKiroAuth) tokenExpiringSoon(expiresAt string) bool {
|
||||
if expiresAt == "" {
|
||||
return false
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339Nano, expiresAt)
|
||||
if err != nil {
|
||||
t, err = time.Parse(time.RFC3339, expiresAt)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return time.Until(t) < kiroTokenExpiryBuffer+s.jitter
|
||||
}
|
||||
|
||||
// refreshFromSSOCache attempts to refresh using the AWS SSO cache as a
|
||||
// fallback when the primary OIDC refresh token is invalid/expired.
|
||||
func (s *sqliteKiroAuth) refreshFromSSOCache(ctx context.Context, previousAccess string) error {
|
||||
cacheState, err := kiroLoadSSOCacheAuthState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(cacheState.RefreshToken) == "" ||
|
||||
strings.TrimSpace(cacheState.Region) == "" ||
|
||||
strings.TrimSpace(cacheState.ClientID) == "" ||
|
||||
strings.TrimSpace(cacheState.ClientSecret) == "" {
|
||||
return fmt.Errorf("SSO cache does not contain enough data to refresh")
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("https://oidc.%s.amazonaws.com/token", strings.TrimSpace(cacheState.Region))
|
||||
client := newKiroOIDCClient(endpoint, nil)
|
||||
|
||||
resp, err := client.RefreshToken(ctx, kiroRefreshTokenRequest{
|
||||
ClientID: cacheState.ClientID,
|
||||
ClientSecret: cacheState.ClientSecret,
|
||||
GrantType: "refresh_token",
|
||||
RefreshToken: cacheState.RefreshToken,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newAccess := strings.TrimSpace(resp.AccessToken)
|
||||
newRefresh := strings.TrimSpace(resp.RefreshToken)
|
||||
var expiresAt string
|
||||
if resp.ExpiresIn != nil && *resp.ExpiresIn > 0 {
|
||||
expiresAt = time.Now().Add(time.Duration(*resp.ExpiresIn) * time.Second).UTC().Format(time.RFC3339)
|
||||
}
|
||||
if err := kiroUpdateSQLiteTokens(ctx, s.path, newAccess, newRefresh, expiresAt); err != nil {
|
||||
if s.sqliteHasNewUsableAccessToken(ctx, previousAccess) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
// Also update SSO cache file.
|
||||
_ = kiroUpdateSSOCacheToken(cacheState.TokenPath, newAccess, newRefresh, expiresAt)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sqliteHasNewUsableAccessToken retries reading the SQLite token to detect if
|
||||
// another process refreshed during our attempt.
|
||||
func (s *sqliteKiroAuth) sqliteHasNewUsableAccessToken(ctx context.Context, previousAccess string) bool {
|
||||
const attempts = 3
|
||||
const backoff = 500 * time.Millisecond
|
||||
for i := range attempts {
|
||||
if i > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
case <-time.After(backoff):
|
||||
}
|
||||
}
|
||||
access, _, expiresAt, err := kiroReadSQLiteTokens(ctx, s.path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if access != "" && access != strings.TrimSpace(previousAccess) && !s.tokenExpiringSoon(expiresAt) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const kiroTokenExpiryBuffer = 2 * time.Minute
|
||||
|
||||
// kiroTokenExpiryJitter is the maximum random offset added to kiroTokenExpiryBuffer.
|
||||
const kiroTokenExpiryJitter = 2 * time.Minute
|
||||
|
||||
func kiroRandomJitter() time.Duration {
|
||||
b := make([]byte, 4)
|
||||
_, _ = rand.Read(b)
|
||||
n := uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
|
||||
return time.Duration(n%uint32(kiroTokenExpiryJitter/time.Millisecond)) * time.Millisecond
|
||||
}
|
||||
|
||||
func kiroTokenExpiringSoon(expiresAt string) bool {
|
||||
if expiresAt == "" {
|
||||
return false
|
||||
|
|
@ -972,23 +1217,161 @@ func kiroQuerySQLiteOptional(ctx context.Context, path, query string) (string, b
|
|||
return "", false, err
|
||||
}
|
||||
|
||||
// sqliteBusyTimeout is the per-connection busy timeout passed to the sqlite3
|
||||
// CLI via -cmd. This is session-scoped and does not modify the DB file.
|
||||
const kiroSQLiteBusyTimeout = "5000"
|
||||
|
||||
// sqliteBusyRetries is the number of times we retry at the Go level when
|
||||
// sqlite3 still returns a busy/locked error after its internal timeout.
|
||||
const kiroSQLiteBusyRetries = 2
|
||||
|
||||
// sqliteBusyBackoff is the delay between Go-level retries.
|
||||
const kiroSQLiteBusyBackoff = 200 * time.Millisecond
|
||||
|
||||
func kiroRunSQLite(ctx context.Context, args ...string) ([]byte, error) {
|
||||
cmd := exec.CommandContext(ctx, "sqlite3", args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
msg := strings.TrimSpace(string(out))
|
||||
if msg != "" {
|
||||
return nil, fmt.Errorf("%s", msg)
|
||||
// Prepend busy timeout so sqlite3 retries internally on lock contention.
|
||||
fullArgs := make([]string, 0, len(args)+2)
|
||||
fullArgs = append(fullArgs, "-cmd", ".timeout "+kiroSQLiteBusyTimeout)
|
||||
fullArgs = append(fullArgs, args...)
|
||||
|
||||
var out []byte
|
||||
var err error
|
||||
for attempt := range kiroSQLiteBusyRetries + 1 {
|
||||
if attempt > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return out, ctx.Err()
|
||||
case <-time.After(kiroSQLiteBusyBackoff):
|
||||
}
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "sqlite3", fullArgs...)
|
||||
out, err = cmd.CombinedOutput()
|
||||
if err == nil || !kiroIsSQLiteBusy(out, err) {
|
||||
return out, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
return out, err
|
||||
}
|
||||
|
||||
// kiroIsSQLiteBusy checks whether the sqlite3 invocation failed due to lock
|
||||
// contention. The sqlite3 CLI returns exit code 5 (SQLITE_BUSY) or 6
|
||||
// (SQLITE_LOCKED).
|
||||
func kiroIsSQLiteBusy(output []byte, err error) bool {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
code := exitErr.ExitCode()
|
||||
if code == 5 || code == 6 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
s := strings.ToLower(string(output))
|
||||
return strings.Contains(s, "database is locked") ||
|
||||
strings.Contains(s, "database table is locked")
|
||||
}
|
||||
|
||||
func kiroSQLiteQuote(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", "''") + "'"
|
||||
}
|
||||
|
||||
// ── SSO cache fallback ────────────────────────────────────────────────────────
|
||||
|
||||
type kiroSSOCacheState struct {
|
||||
TokenPath string
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
ExpiresAt string
|
||||
Region string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
func kiroLoadSSOCacheAuthState() (kiroSSOCacheState, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return kiroSSOCacheState{}, fmt.Errorf("cannot determine home directory for SSO cache: %w", err)
|
||||
}
|
||||
cacheDir := filepath.Join(homeDir, ".aws", "sso", "cache")
|
||||
tokenPath := filepath.Join(cacheDir, "kiro-auth-token-cli.json")
|
||||
tokenBytes, err := os.ReadFile(tokenPath)
|
||||
if err != nil {
|
||||
return kiroSSOCacheState{}, fmt.Errorf("read SSO token cache %q: %w", tokenPath, err)
|
||||
}
|
||||
var tokenState struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
ExpiresAt string `json:"expiresAt"`
|
||||
Region string `json:"region"`
|
||||
ClientIDHash string `json:"clientIdHash"`
|
||||
}
|
||||
if err := json.Unmarshal(tokenBytes, &tokenState); err != nil {
|
||||
return kiroSSOCacheState{}, fmt.Errorf("decode SSO token cache %q: %w", tokenPath, err)
|
||||
}
|
||||
clientIDHash := strings.TrimSpace(tokenState.ClientIDHash)
|
||||
if clientIDHash == "" {
|
||||
return kiroSSOCacheState{}, fmt.Errorf("SSO token cache %q did not contain clientIdHash", tokenPath)
|
||||
}
|
||||
clientPath := filepath.Join(cacheDir, clientIDHash+".json")
|
||||
clientBytes, err := os.ReadFile(clientPath)
|
||||
if err != nil {
|
||||
return kiroSSOCacheState{}, fmt.Errorf("read SSO client cache %q: %w", clientPath, err)
|
||||
}
|
||||
var clientState struct {
|
||||
ClientID string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
}
|
||||
if err := json.Unmarshal(clientBytes, &clientState); err != nil {
|
||||
return kiroSSOCacheState{}, fmt.Errorf("decode SSO client cache %q: %w", clientPath, err)
|
||||
}
|
||||
return kiroSSOCacheState{
|
||||
TokenPath: tokenPath,
|
||||
AccessToken: strings.TrimSpace(tokenState.AccessToken),
|
||||
RefreshToken: strings.TrimSpace(tokenState.RefreshToken),
|
||||
ExpiresAt: strings.TrimSpace(tokenState.ExpiresAt),
|
||||
Region: strings.TrimSpace(tokenState.Region),
|
||||
ClientID: strings.TrimSpace(clientState.ClientID),
|
||||
ClientSecret: strings.TrimSpace(clientState.ClientSecret),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func kiroUpdateSSOCacheToken(path, accessToken, refreshToken, expiresAt string) error {
|
||||
accessToken = strings.TrimSpace(accessToken)
|
||||
if accessToken == "" {
|
||||
return fmt.Errorf("access token must not be empty")
|
||||
}
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read SSO token cache %q: %w", path, err)
|
||||
}
|
||||
var tokenState map[string]any
|
||||
if err := json.Unmarshal(body, &tokenState); err != nil {
|
||||
return fmt.Errorf("decode SSO token cache %q: %w", path, err)
|
||||
}
|
||||
tokenState["accessToken"] = accessToken
|
||||
if strings.TrimSpace(refreshToken) != "" {
|
||||
tokenState["refreshToken"] = strings.TrimSpace(refreshToken)
|
||||
}
|
||||
if strings.TrimSpace(expiresAt) != "" {
|
||||
tokenState["expiresAt"] = strings.TrimSpace(expiresAt)
|
||||
}
|
||||
updated, err := json.MarshalIndent(tokenState, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode SSO token cache %q: %w", path, err)
|
||||
}
|
||||
updated = append(updated, '\n')
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stat SSO token cache %q: %w", path, err)
|
||||
}
|
||||
tmpPath := path + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, updated, info.Mode().Perm()); err != nil {
|
||||
return fmt.Errorf("write SSO token cache temp %q: %w", tmpPath, err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, path); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("rename SSO token cache %q: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Auth source factory ───────────────────────────────────────────────────────
|
||||
|
||||
func newKiroAuthSource(apiKey string) (kiroAuthSource, error) {
|
||||
|
|
@ -1012,7 +1395,7 @@ func newKiroAuthSource(apiKey string) (kiroAuthSource, error) {
|
|||
if strings.TrimSpace(path) == "" {
|
||||
return nil, fmt.Errorf("sqlite URL %q does not contain a database path", apiKey)
|
||||
}
|
||||
return &sqliteKiroAuth{path: path}, nil
|
||||
return &sqliteKiroAuth{path: path, jitter: kiroRandomJitter()}, nil
|
||||
}
|
||||
|
||||
func kiroDefaultSQLiteKey() string {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -33,11 +34,11 @@ const (
|
|||
kiroAgentTaskType = "vibe"
|
||||
kiroAmzSDKRequest = "attempt=1; max=3"
|
||||
|
||||
kiroUserAgent = "aws-sdk-rust/1.3.14 ua/2.1 api/codewhispererstreaming/0.1.16551 os/linux lang/rust/1.92.0 md/appVersion-0.12.333 app/AmazonQ-For-CLI"
|
||||
kiroAmzUserAgent = "aws-sdk-rust/1.3.14 ua/2.1 api/codewhispererstreaming/0.1.16551 os/linux lang/rust/1.92.0 m/F app/AmazonQ-For-CLI"
|
||||
kiroUserAgent = "aws-sdk-rust/1.3.15 ua/2.1 api/codewhispererstreaming/0.1.16551 os/linux lang/rust/1.92.0 md/appVersion-2.5.1 app/AmazonQ-For-CLI"
|
||||
kiroAmzUserAgent = "aws-sdk-rust/1.3.15 ua/2.1 api/codewhispererstreaming/0.1.16551 os/linux lang/rust/1.92.0 m/F app/AmazonQ-For-CLI"
|
||||
|
||||
kiroOIDCUserAgent = "aws-sdk-rust/1.3.10 os/linux lang/rust/1.92.0"
|
||||
kiroOIDCAmzUserAgent = "aws-sdk-rust/1.3.10 ua/2.1 api/ssooidc/1.92.0 os/linux lang/rust/1.92.0 m/E app/AmazonQ-For-CLI"
|
||||
kiroOIDCUserAgent = "aws-sdk-rust/1.3.15 os/linux lang/rust/1.92.0"
|
||||
kiroOIDCAmzUserAgent = "aws-sdk-rust/1.3.15 ua/2.1 api/ssooidc/1.100.0 os/linux lang/rust/1.92.0 m/E,N app/AmazonQ-For-CLI"
|
||||
)
|
||||
|
||||
type kiroRefreshTokenRequest struct {
|
||||
|
|
@ -54,8 +55,24 @@ type kiroRefreshTokenResponse struct {
|
|||
}
|
||||
|
||||
type kiroGenerateRequest struct {
|
||||
ConversationState kiroConversationState `json:"conversationState"`
|
||||
ProfileARN string `json:"profileArn,omitempty"`
|
||||
ConversationState kiroConversationState `json:"conversationState"`
|
||||
ProfileARN string `json:"profileArn,omitempty"`
|
||||
AdditionalModelRequestFields *kiroModelRequestFields `json:"additionalModelRequestFields,omitempty"`
|
||||
}
|
||||
|
||||
type kiroModelRequestFields struct {
|
||||
Thinking *kiroThinkingConfig `json:"thinking,omitempty"`
|
||||
OutputConfig *kiroOutputConfig `json:"output_config,omitempty"`
|
||||
MaxTokens *int `json:"max_tokens,omitempty"`
|
||||
}
|
||||
|
||||
type kiroThinkingConfig struct {
|
||||
Type string `json:"type"`
|
||||
Display string `json:"display,omitempty"`
|
||||
}
|
||||
|
||||
type kiroOutputConfig struct {
|
||||
Effort string `json:"effort,omitempty"`
|
||||
}
|
||||
|
||||
type kiroConversationState struct {
|
||||
|
|
@ -73,18 +90,13 @@ type kiroChatMessage struct {
|
|||
}
|
||||
|
||||
type kiroUserInputMessage struct {
|
||||
Content json.RawMessage `json:"content"`
|
||||
Content string `json:"content"`
|
||||
UserInputMessageContext *kiroUserInputContext `json:"userInputMessageContext,omitempty"`
|
||||
Origin string `json:"origin,omitempty"`
|
||||
Images []kiroImageBlock `json:"images,omitempty"`
|
||||
ModelID string `json:"modelId,omitempty"`
|
||||
}
|
||||
|
||||
type kiroContentBlock struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
Image *kiroImageBlock `json:"image,omitempty"`
|
||||
}
|
||||
|
||||
type kiroImageBlock struct {
|
||||
Format string `json:"format"`
|
||||
Source kiroImageSource `json:"source"`
|
||||
|
|
@ -97,6 +109,12 @@ type kiroImageSource struct {
|
|||
type kiroUserInputContext struct {
|
||||
ToolResults []kiroToolResult `json:"toolResults,omitempty"`
|
||||
Tools []kiroTool `json:"tools,omitempty"`
|
||||
EnvState *kiroEnvState `json:"envState,omitempty"`
|
||||
}
|
||||
|
||||
type kiroEnvState struct {
|
||||
OperatingSystem string `json:"operatingSystem,omitempty"`
|
||||
CurrentWorkingDirectory string `json:"currentWorkingDirectory,omitempty"`
|
||||
}
|
||||
|
||||
type kiroAssistantResponseMessage struct {
|
||||
|
|
@ -145,6 +163,12 @@ type kiroAssistantEvent struct {
|
|||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type kiroReasoningContentEvent struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
RedactedContent string `json:"redactedContent,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
}
|
||||
|
||||
type kiroToolUseEvent struct {
|
||||
ToolUseID string `json:"toolUseId"`
|
||||
Name string `json:"name"`
|
||||
|
|
@ -350,11 +374,22 @@ func (e *kiroAPIError) isThrottled() bool {
|
|||
}
|
||||
|
||||
func (e *kiroAPIError) isTransient() bool {
|
||||
return e.StatusCode == http.StatusInternalServerError || e.Code == "InternalServerException"
|
||||
switch e.StatusCode {
|
||||
case http.StatusRequestTimeout, // 408
|
||||
http.StatusInternalServerError, // 500
|
||||
http.StatusBadGateway, // 502
|
||||
http.StatusServiceUnavailable: // 503
|
||||
return true
|
||||
}
|
||||
code := strings.ToLower(e.Code)
|
||||
return strings.Contains(code, "insufficientmodelcapacity") ||
|
||||
strings.Contains(code, "serviceunavailable") ||
|
||||
strings.Contains(code, "internalserver")
|
||||
}
|
||||
|
||||
type kiroStreamCallbacks struct {
|
||||
OnAssistantDelta func(kiroAssistantEvent)
|
||||
OnReasoningDelta func(kiroReasoningContentEvent)
|
||||
}
|
||||
|
||||
func newKiroAPIClient(endpoint, token string, extraHeaders map[string]string, httpClient *http.Client) *kiroAPIClient {
|
||||
|
|
@ -426,6 +461,14 @@ func (c *kiroAPIClient) Stream(
|
|||
if callbacks.OnAssistantDelta != nil {
|
||||
callbacks.OnAssistantDelta(event)
|
||||
}
|
||||
case "reasoningContentEvent":
|
||||
var event kiroReasoningContentEvent
|
||||
if err := json.Unmarshal(frame.Payload, &event); err != nil {
|
||||
return nil, fmt.Errorf("decode reasoning content event: %w", err)
|
||||
}
|
||||
if callbacks.OnReasoningDelta != nil {
|
||||
callbacks.OnReasoningDelta(event)
|
||||
}
|
||||
case "toolUseEvent":
|
||||
var event kiroToolUseEvent
|
||||
if err := json.Unmarshal(frame.Payload, &event); err != nil {
|
||||
|
|
@ -483,7 +526,7 @@ func (c *kiroAPIClient) applyHeaders(req *http.Request) error {
|
|||
req.Header.Set("X-Amz-User-Agent", kiroAmzUserAgent)
|
||||
req.Header.Set("Amz-Sdk-Request", kiroAmzSDKRequest)
|
||||
req.Header.Set("Amz-Sdk-Invocation-Id", invocationID)
|
||||
req.Header.Set("X-Amzn-Codewhisperer-Optout", "false")
|
||||
req.Header.Set("X-Amzn-Codewhisperer-Optout", "true")
|
||||
for k, v := range c.extraHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
|
@ -553,11 +596,25 @@ func kiroDefaultEndpoint(profileARN string) (string, error) {
|
|||
if region == "" {
|
||||
return "", fmt.Errorf("profile ARN %q does not contain a region", profileARN)
|
||||
}
|
||||
return "https://q." + region + ".amazonaws.com/", nil
|
||||
return "https://runtime." + region + ".kiro.dev/", nil
|
||||
}
|
||||
|
||||
func kiroEndpointForRegion(region string) string {
|
||||
return "https://q." + strings.TrimSpace(region) + ".amazonaws.com/"
|
||||
return "https://runtime." + strings.TrimSpace(region) + ".kiro.dev/"
|
||||
}
|
||||
|
||||
func kiroManagementEndpointFor(endpoint string) (string, error) {
|
||||
parsed, err := url.Parse(strings.TrimSpace(endpoint))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse endpoint: %w", err)
|
||||
}
|
||||
if parsed.Host == "" {
|
||||
return "", fmt.Errorf("endpoint %q does not contain a host", endpoint)
|
||||
}
|
||||
if strings.HasPrefix(parsed.Host, "runtime.") && strings.HasSuffix(parsed.Host, ".kiro.dev") {
|
||||
parsed.Host = "management." + strings.TrimPrefix(parsed.Host, "runtime.")
|
||||
}
|
||||
return strings.TrimRight(parsed.String(), "/") + "/", nil
|
||||
}
|
||||
|
||||
func kiroNewUUID() (string, error) {
|
||||
|
|
@ -638,8 +695,8 @@ func (c *kiroOIDCClient) RefreshToken(ctx context.Context, req kiroRefreshTokenR
|
|||
|
||||
const (
|
||||
kiroListModelsTarget = "AmazonCodeWhispererService.ListAvailableModels"
|
||||
kiroListModelsUserAgent = "aws-sdk-rust/1.3.14 ua/2.1 api/codewhispererruntime/0.1.16551 os/linux lang/rust/1.92.0 md/appVersion-0.12.333 app/AmazonQ-For-CLI"
|
||||
kiroListModelsAmzUserAgent = "aws-sdk-rust/1.3.14 ua/2.1 api/codewhispererruntime/0.1.16551 os/linux lang/rust/1.92.0 m/F,C app/AmazonQ-For-CLI"
|
||||
kiroListModelsUserAgent = "aws-sdk-rust/1.3.15 ua/2.1 api/codewhispererruntime/0.1.16551 os/linux lang/rust/1.92.0 md/appVersion-2.5.1 app/AmazonQ-For-CLI"
|
||||
kiroListModelsAmzUserAgent = "aws-sdk-rust/1.3.15 ua/2.1 api/codewhispererruntime/0.1.16551 os/linux lang/rust/1.92.0 m/F,C app/AmazonQ-For-CLI"
|
||||
)
|
||||
|
||||
type kiroListModelsRequest struct {
|
||||
|
|
@ -670,7 +727,24 @@ func (c *kiroAPIClient) ListModels(ctx context.Context, profileARN string) (*kir
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint, bytes.NewReader(body))
|
||||
// ListModels uses the management endpoint (management.*.kiro.dev) rather
|
||||
// than the streaming endpoint (runtime.*.kiro.dev).
|
||||
mgmtEndpoint, err := kiroManagementEndpointFor(c.endpoint)
|
||||
if err != nil {
|
||||
mgmtEndpoint = c.endpoint // fall back to streaming endpoint
|
||||
}
|
||||
targetURL, err := url.Parse(mgmtEndpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse management endpoint: %w", err)
|
||||
}
|
||||
query := targetURL.Query()
|
||||
query.Set("origin", kiroDefaultOrigin)
|
||||
if profileARN != "" {
|
||||
query.Set("profileArn", profileARN)
|
||||
}
|
||||
targetURL.RawQuery = query.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, targetURL.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
Reference in New Issue