diff --git a/CHANGELOG.md b/CHANGELOG.md index 13f2070..61ac64d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ existing `0.1.0` release; earlier development prereleases are not listed. ## [Unreleased] +### Fixed + +- Align Managed Session budgets with the official decimal-string `max_credit_cost` contract, shared by Deployment budgets. The limit covers cumulative model and sandbox Credits; reaching it rejects the next user message without interrupting the current turn. +- Expose Managed and Forward Session `model_credits`, `sandbox_runtime_credits`, and `total_credits`; decode top-level `session.usage` Credits and per-call `model_usage.credits` / compaction `usage.credits`. Session usage may be absent. Legacy response token/list-cost fields remain available. +- Route Identity ownership through query parameters on existing Forward Environment, Skill/Version, Vault/Credential, and File operations, including multipart uploads and content downloads. +- Allow existing channel pairing creation with either a code or `channel_pairing_id`, and expose pairing scope details. No additional API endpoints are introduced. + +### Migration + +- **Breaking budget field change:** Replace `MaxListCost: managed.MonetaryAmountParam{...}` with `MaxCreditCost: "100.00"` in `ManagedAgentsBudgetLimitParam` (keep `Type: "limit"`). Amounts are positive decimal strings, not currency objects. Omitting a budget update preserves the limit; explicit null removes it. +- Consume the three top-level Credits fields from `session.usage` events. Replace cumulative snapshots rather than adding them; compaction Credits are already included in Session totals. +- Set `IdentityID: forward.String("idn_...")` in resource params; for methods without params, pass `forward.WithIdentityID("idn_...")`. Use this only with a PAT. With a SAT, omit `identity_id`; the server infers ownership from the token and rejects explicit values (including an empty string). +- Channel pairing requests must provide exactly one of `code` or `channel_pairing_id`; existing code-based calls remain supported. + ## [0.6.0] ### Added diff --git a/docs/api/reference.md b/docs/api/reference.md index 3d62b28..0cfc698 100644 --- a/docs/api/reference.md +++ b/docs/api/reference.md @@ -13,6 +13,7 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward" - [func Float\(v float64\) param.Opt\[float64\]](<#Float>) - [func Int\(v int64\) param.Opt\[int64\]](<#Int>) - [func String\(v string\) param.Opt\[string\]](<#String>) +- [func WithIdentityID\(identityID string\) option.RequestOption](<#WithIdentityID>) - [type Batch](<#Batch>) - [func \(r Batch\) RawJSON\(\) string](<#Batch.RawJSON>) - [func \(r \*Batch\) UnmarshalJSON\(data \[\]byte\) error](<#Batch.UnmarshalJSON>) @@ -209,6 +210,7 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward" - [func \(r EnvironmentListParams\) URLQuery\(\) \(url.Values, error\)](<#EnvironmentListParams.URLQuery>) - [type EnvironmentNewParams](<#EnvironmentNewParams>) - [func \(r EnvironmentNewParams\) MarshalJSON\(\) \(\[\]byte, error\)](<#EnvironmentNewParams.MarshalJSON>) + - [func \(r EnvironmentNewParams\) URLQuery\(\) \(url.Values, error\)](<#EnvironmentNewParams.URLQuery>) - [func \(r \*EnvironmentNewParams\) UnmarshalJSON\(data \[\]byte\) error](<#EnvironmentNewParams.UnmarshalJSON>) - [type EnvironmentService](<#EnvironmentService>) - [func NewEnvironmentService\(opts ...option.RequestOption\) EnvironmentService](<#NewEnvironmentService>) @@ -221,6 +223,7 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward" - [func \(r \*EnvironmentService\) Update\(ctx context.Context, id string, params EnvironmentUpdateParams, opts ...option.RequestOption\) \(res \*Environment, err error\)](<#EnvironmentService.Update>) - [type EnvironmentUpdateParams](<#EnvironmentUpdateParams>) - [func \(r EnvironmentUpdateParams\) MarshalJSON\(\) \(\[\]byte, error\)](<#EnvironmentUpdateParams.MarshalJSON>) + - [func \(r EnvironmentUpdateParams\) URLQuery\(\) \(url.Values, error\)](<#EnvironmentUpdateParams.URLQuery>) - [func \(r \*EnvironmentUpdateParams\) UnmarshalJSON\(data \[\]byte\) error](<#EnvironmentUpdateParams.UnmarshalJSON>) - [type EnvironmentVariableOverride](<#EnvironmentVariableOverride>) - [func \(r EnvironmentVariableOverride\) RawJSON\(\) string](<#EnvironmentVariableOverride.RawJSON>) @@ -253,6 +256,7 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward" - [func \(r \*FileService\) Upload\(ctx context.Context, params FileUploadParams, opts ...option.RequestOption\) \(res \*FileMetadata, err error\)](<#FileService.Upload>) - [type FileUploadParams](<#FileUploadParams>) - [func \(r FileUploadParams\) MarshalMultipart\(\) \(\[\]byte, string, error\)](<#FileUploadParams.MarshalMultipart>) + - [func \(r FileUploadParams\) URLQuery\(\) \(url.Values, error\)](<#FileUploadParams.URLQuery>) - [type GitHubRepository](<#GitHubRepository>) - [func \(r GitHubRepository\) RawJSON\(\) string](<#GitHubRepository.RawJSON>) - [func \(r \*GitHubRepository\) UnmarshalJSON\(data \[\]byte\) error](<#GitHubRepository.UnmarshalJSON>) @@ -761,6 +765,7 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward" - [func \(r SkillListParams\) URLQuery\(\) \(url.Values, error\)](<#SkillListParams.URLQuery>) - [type SkillNewParams](<#SkillNewParams>) - [func \(r SkillNewParams\) MarshalMultipart\(\) \(\[\]byte, string, error\)](<#SkillNewParams.MarshalMultipart>) + - [func \(r SkillNewParams\) URLQuery\(\) \(url.Values, error\)](<#SkillNewParams.URLQuery>) - [type SkillOverride](<#SkillOverride>) - [func \(r SkillOverride\) RawJSON\(\) string](<#SkillOverride.RawJSON>) - [func \(r \*SkillOverride\) UnmarshalJSON\(data \[\]byte\) error](<#SkillOverride.UnmarshalJSON>) @@ -777,6 +782,7 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward" - [func \(r \*SkillService\) Update\(ctx context.Context, id string, params SkillUpdateParams, opts ...option.RequestOption\) \(res \*Skill, err error\)](<#SkillService.Update>) - [type SkillUpdateParams](<#SkillUpdateParams>) - [func \(r SkillUpdateParams\) MarshalJSON\(\) \(\[\]byte, error\)](<#SkillUpdateParams.MarshalJSON>) + - [func \(r SkillUpdateParams\) URLQuery\(\) \(url.Values, error\)](<#SkillUpdateParams.URLQuery>) - [func \(r \*SkillUpdateParams\) UnmarshalJSON\(data \[\]byte\) error](<#SkillUpdateParams.UnmarshalJSON>) - [type SkillVersion](<#SkillVersion>) - [func \(r SkillVersion\) RawJSON\(\) string](<#SkillVersion.RawJSON>) @@ -785,6 +791,7 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward" - [func \(r SkillVersionListParams\) URLQuery\(\) \(url.Values, error\)](<#SkillVersionListParams.URLQuery>) - [type SkillVersionNewParams](<#SkillVersionNewParams>) - [func \(r SkillVersionNewParams\) MarshalMultipart\(\) \(\[\]byte, string, error\)](<#SkillVersionNewParams.MarshalMultipart>) + - [func \(r SkillVersionNewParams\) URLQuery\(\) \(url.Values, error\)](<#SkillVersionNewParams.URLQuery>) - [type SkillVersionService](<#SkillVersionService>) - [func NewSkillVersionService\(opts ...option.RequestOption\) SkillVersionService](<#NewSkillVersionService>) - [func \(r \*SkillVersionService\) Delete\(ctx context.Context, id string, version string, opts ...option.RequestOption\) \(res \*DeletedSkillVersion, err error\)](<#SkillVersionService.Delete>) @@ -870,6 +877,7 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward" - [func \(r VaultCredentialListParams\) URLQuery\(\) \(url.Values, error\)](<#VaultCredentialListParams.URLQuery>) - [type VaultCredentialNewParams](<#VaultCredentialNewParams>) - [func \(r VaultCredentialNewParams\) MarshalJSON\(\) \(\[\]byte, error\)](<#VaultCredentialNewParams.MarshalJSON>) + - [func \(r VaultCredentialNewParams\) URLQuery\(\) \(url.Values, error\)](<#VaultCredentialNewParams.URLQuery>) - [func \(r \*VaultCredentialNewParams\) UnmarshalJSON\(data \[\]byte\) error](<#VaultCredentialNewParams.UnmarshalJSON>) - [type VaultCredentialService](<#VaultCredentialService>) - [func NewVaultCredentialService\(opts ...option.RequestOption\) VaultCredentialService](<#NewVaultCredentialService>) @@ -890,6 +898,7 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward" - [func \(r VaultListParams\) URLQuery\(\) \(url.Values, error\)](<#VaultListParams.URLQuery>) - [type VaultNewParams](<#VaultNewParams>) - [func \(r VaultNewParams\) MarshalJSON\(\) \(\[\]byte, error\)](<#VaultNewParams.MarshalJSON>) + - [func \(r VaultNewParams\) URLQuery\(\) \(url.Values, error\)](<#VaultNewParams.URLQuery>) - [func \(r \*VaultNewParams\) UnmarshalJSON\(data \[\]byte\) error](<#VaultNewParams.UnmarshalJSON>) - [type VaultService](<#VaultService>) - [func NewVaultService\(opts ...option.RequestOption\) VaultService](<#NewVaultService>) @@ -956,6 +965,15 @@ func String(v string) param.Opt[string] + +## func [WithIdentityID]() + +```go +func WithIdentityID(identityID string) option.RequestOption +``` + +WithIdentityID selects identity ownership for Forward resources using a PAT. Omit this option when using a SAT; its identity is inferred by the server. Use it on retrieval, deletion, archive, and download methods without params. + ## type [Batch]() @@ -1821,6 +1839,11 @@ func (r *ChannelNewParams) UnmarshalJSON(data []byte) error ```go type ChannelPairing struct { + ScopeType string `json:"scope_type"` + ScopeExternalID string `json:"scope_external_id"` + ScopeDisplayName string `json:"scope_display_name"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` // Pairing ID, used when removing the pairing. ID string `json:"id"` // Always `channel_pairing`. @@ -1836,14 +1859,19 @@ type ChannelPairing struct { // Time the pairing completed. PairedAt string `json:"paired_at"` JSON struct { - ID respjson.Field - Type respjson.Field - ChannelID respjson.Field - IdentityID respjson.Field - TemplateID respjson.Field - Status respjson.Field - PairedAt respjson.Field - ExtraFields map[string]respjson.Field + ScopeType respjson.Field + ScopeExternalID respjson.Field + ScopeDisplayName respjson.Field + CreatedAt respjson.Field + UpdatedAt respjson.Field + ID respjson.Field + Type respjson.Field + ChannelID respjson.Field + IdentityID respjson.Field + TemplateID respjson.Field + Status respjson.Field + PairedAt respjson.Field + ExtraFields map[string]respjson.Field // contains filtered or unexported fields } `json:"-"` } @@ -1874,8 +1902,10 @@ func (r *ChannelPairing) UnmarshalJSON(data []byte) error ```go type ChannelPairingNewParams struct { - // Six-digit pairing code shown in the Channel message. - Code string `json:"code" api:"required"` + // Pairing code. Supply exactly one of Code or ChannelPairingID. + Code string `json:"code,omitzero"` + // Pending pairing ID. Supply exactly one of Code or ChannelPairingID. + ChannelPairingID string `json:"channel_pairing_id,omitzero"` // Forward Identity ID to bind. IdentityID string `json:"identity_id" api:"required"` // Forward Template ID to bind. @@ -3441,6 +3471,8 @@ func (r *EnvironmentConfigPackages) UnmarshalJSON(data []byte) error ```go type EnvironmentListParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Page size, maximum 100. Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Pagination cursor (recommended); take the value from `next_page` in the previous @@ -3470,6 +3502,8 @@ func (r EnvironmentListParams) URLQuery() (url.Values, error) ```go type EnvironmentNewParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Environment name; must not be empty after trimming surrounding whitespace. Name string `json:"name" api:"required"` // Description. @@ -3497,6 +3531,15 @@ func (r EnvironmentNewParams) MarshalJSON() ([]byte, error) + +### func \(EnvironmentNewParams\) [URLQuery]() + +```go +func (r EnvironmentNewParams) URLQuery() (url.Values, error) +``` + + + ### func \(\*EnvironmentNewParams\) [UnmarshalJSON]() @@ -3596,6 +3639,8 @@ Update Environment ```go type EnvironmentUpdateParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // New name. Name param.Opt[string] `json:"name,omitzero"` // New description. @@ -3619,6 +3664,15 @@ func (r EnvironmentUpdateParams) MarshalJSON() ([]byte, error) + +### func \(EnvironmentUpdateParams\) [URLQuery]() + +```go +func (r EnvironmentUpdateParams) URLQuery() (url.Values, error) +``` + + + ### func \(\*EnvironmentUpdateParams\) [UnmarshalJSON]() @@ -3805,6 +3859,8 @@ func (r *EventContentUnionParam) UnmarshalJSON(data []byte) error ```go type FileListParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Page size, maximum 100. Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Pagination cursor (recommended), taken from `next_page` in the previous response; @@ -3979,6 +4035,8 @@ Upload File ```go type FileUploadParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Content of the file to upload. For accepted types see // [Supported upload file types](./schemas.md). File io.Reader `json:"file" api:"required" format:"binary"` @@ -4008,6 +4066,15 @@ func (r FileUploadParams) MarshalMultipart() ([]byte, string, error) + +### func \(FileUploadParams\) [URLQuery]() + +```go +func (r FileUploadParams) URLQuery() (url.Values, error) +``` + + + ## type [GitHubRepository]() @@ -10039,10 +10106,14 @@ func (r *SessionUpdateParamsConfigParam) UnmarshalJSON(data []byte) error ```go type SessionUsage struct { - TotalCredits float64 `json:"total_credits"` - JSON struct { - TotalCredits respjson.Field - ExtraFields map[string]respjson.Field + TotalCredits float64 `json:"total_credits"` + ModelCredits float64 `json:"model_credits"` + SandboxRuntimeCredits float64 `json:"sandbox_runtime_credits"` + JSON struct { + ModelCredits respjson.Field + SandboxRuntimeCredits respjson.Field + TotalCredits respjson.Field + ExtraFields map[string]respjson.Field // contains filtered or unexported fields } `json:"-"` } @@ -10202,6 +10273,8 @@ func (r *SkillBindingParam) UnmarshalJSON(data []byte) error ```go type SkillGetParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // ⚠️ **Deprecated**: when `true`, the response also carries `content` and // `content_encoding` (a base64 zip). Responses carry the `Deprecation: true` header // when it is used. Use [Download Skill version content](./Versions/download.md) @@ -10227,6 +10300,8 @@ func (r SkillGetParams) URLQuery() (url.Values, error) ```go type SkillListParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Page size, maximum 100. Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Pagination cursor (recommended), taken from `next_page` in the previous response; @@ -10264,6 +10339,8 @@ func (r SkillListParams) URLQuery() (url.Values, error) ```go type SkillNewParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Recommended upload field, which may **repeat** several times. Two shapes are // supported: (1) a single `.zip` archive; (2) a bare file tree — each part uploads // one file and carries its relative path in `filename` (such as @@ -10304,6 +10381,15 @@ func (r SkillNewParams) MarshalMultipart() ([]byte, string, error) + +### func \(SkillNewParams\) [URLQuery]() + +```go +func (r SkillNewParams) URLQuery() (url.Values, error) +``` + + + ## type [SkillOverride]() @@ -10456,6 +10542,8 @@ Update Skill ```go type SkillUpdateParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // New description. Description param.Opt[string] `json:"description,omitzero"` // New content (the contents of a zip archive). Neither the archive itself nor its @@ -10490,6 +10578,15 @@ func (r SkillUpdateParams) MarshalJSON() ([]byte, error) + +### func \(SkillUpdateParams\) [URLQuery]() + +```go +func (r SkillUpdateParams) URLQuery() (url.Values, error) +``` + + + ### func \(\*SkillUpdateParams\) [UnmarshalJSON]() @@ -10560,6 +10657,8 @@ func (r *SkillVersion) UnmarshalJSON(data []byte) error ```go type SkillVersionListParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Page size, maximum 100, default 20. Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Cursor for paging forward; take the value from `next_page` in the previous @@ -10585,6 +10684,8 @@ func (r SkillVersionListParams) URLQuery() (url.Values, error) ```go type SkillVersionNewParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Upload field, which may appear **multiple** times. Two shapes are supported: • a // single `.zip` archive; • a bare file tree, where every part uploads one file and // `filename` carries its relative path (e.g. `customer-reply/SKILL.md`, @@ -10604,6 +10705,15 @@ func (r SkillVersionNewParams) MarshalMultipart() ([]byte, string, error) + +### func \(SkillVersionNewParams\) [URLQuery]() + +```go +func (r SkillVersionNewParams) URLQuery() (url.Values, error) +``` + + + ## type [SkillVersionService]() @@ -11677,6 +11787,8 @@ func (r *VaultCredentialAuth) UnmarshalJSON(data []byte) error ```go type VaultCredentialListParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Page size, maximum 100. Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Pagination cursor (recommended), taken from `next_page` in the previous response; @@ -11708,6 +11820,8 @@ func (r VaultCredentialListParams) URLQuery() (url.Values, error) ```go type VaultCredentialNewParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Credential authentication material; supports `static_bearer` and `mcp_oauth`. The // response only returns redacted, non-secret fields. Auth map[string]any `json:"auth" api:"required"` @@ -11733,6 +11847,15 @@ func (r VaultCredentialNewParams) MarshalJSON() ([]byte, error) + +### func \(VaultCredentialNewParams\) [URLQuery]() + +```go +func (r VaultCredentialNewParams) URLQuery() (url.Values, error) +``` + + + ### func \(\*VaultCredentialNewParams\) [UnmarshalJSON]() @@ -11896,6 +12019,8 @@ func (r *VaultCredentialUpdateParams) UnmarshalJSON(data []byte) error ```go type VaultListParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Page size, maximum 100. Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Pagination cursor (recommended), taken from `next_page` in the previous @@ -11927,6 +12052,8 @@ func (r VaultListParams) URLQuery() (url.Values, error) ```go type VaultNewParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Vault display name. DisplayName string `json:"display_name" api:"required"` // Metadata object; `created_by` is reserved and cannot be supplied (supplying it @@ -11948,6 +12075,15 @@ func (r VaultNewParams) MarshalJSON() ([]byte, error) + +### func \(VaultNewParams\) [URLQuery]() + +```go +func (r VaultNewParams) URLQuery() (url.Values, error) +``` + + + ### func \(\*VaultNewParams\) [UnmarshalJSON]() @@ -18258,8 +18394,8 @@ type DeploymentNewParams struct { Name string `json:"name" api:"required"` // Description of what the deployment does. Description param.Opt[string] `json:"description,omitzero"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. Budget ManagedAgentsBudgetLimitParam `json:"budget,omitzero"` // Arbitrary key-value metadata. Maximum 16 pairs, keys up to 64 chars, values up // to 512 chars. @@ -18832,8 +18968,8 @@ type DeploymentUpdateParams struct { // version, or an `agent` object with both id and version specified. Omit to // preserve. Cannot be cleared. Agent DeploymentUpdateParamsAgentUnion `json:"agent,omitzero"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. Budget ManagedAgentsBudgetLimitParam `json:"budget,omitzero"` // Initial events. Full replacement. Omit to preserve. Cannot be cleared. At least // 1, maximum 50. @@ -23202,6 +23338,8 @@ Indicates that context compaction \(summarization\) occurred during the session. ```go type ManagedAgentsAgentThreadContextCompactedEvent struct { + // Credits for this compaction call, already included in Session totals. + Usage ManagedAgentsSpanModelUsage `json:"usage"` // Unique identifier for this event. ID string `json:"id" api:"required"` // A timestamp in RFC 3339 format @@ -23210,6 +23348,7 @@ type ManagedAgentsAgentThreadContextCompactedEvent struct { Type ManagedAgentsAgentThreadContextCompactedEventType `json:"type" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { + Usage respjson.Field ID respjson.Field ProcessedAt respjson.Field Type respjson.Field @@ -26101,19 +26240,19 @@ const ( ## type [ManagedAgentsBudgetLimit]() -A hard spend ceiling. The session stops issuing new model requests once the tracked list cost reaches \`max\_list\_cost\`. +A total model and sandbox credit limit. Once reached, new user messages are rejected; an already running turn continues to completion. ```go type ManagedAgentsBudgetLimit struct { - // A monetary amount in a specific currency. - MaxListCost MonetaryAmount `json:"max_list_cost" api:"required"` + // Positive decimal Credits, with at most 12 integer and 2 fractional digits. + MaxCreditCost string `json:"max_credit_cost" api:"required"` // Any of "limit". Type ManagedAgentsBudgetLimitType `json:"type" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - MaxListCost respjson.Field - Type respjson.Field - ExtraFields map[string]respjson.Field + MaxCreditCost respjson.Field + Type respjson.Field + ExtraFields map[string]respjson.Field // contains filtered or unexported fields } `json:"-"` } @@ -26151,14 +26290,14 @@ func (r *ManagedAgentsBudgetLimit) UnmarshalJSON(data []byte) error ## type [ManagedAgentsBudgetLimitParam]() -A hard spend ceiling. The session stops issuing new model requests once the tracked list cost reaches \`max\_list\_cost\`. +A total model and sandbox credit limit. Once reached, new user messages are rejected; an already running turn continues to completion. -The properties MaxListCost, Type are required. +The properties MaxCreditCost, Type are required. ```go type ManagedAgentsBudgetLimitParam struct { - // A monetary amount in a specific currency. - MaxListCost MonetaryAmountParam `json:"max_list_cost,omitzero" api:"required"` + // Positive decimal Credits, with at most 12 integer and 2 fractional digits. + MaxCreditCost string `json:"max_credit_cost" api:"required"` // Any of "limit". Type ManagedAgentsBudgetLimitType `json:"type,omitzero" api:"required"` // contains filtered or unexported fields @@ -27667,8 +27806,8 @@ type ManagedAgentsDeployment struct { // Vault IDs supplying stored credentials for sessions created from this // deployment. VaultIDs []string `json:"vault_ids" api:"required"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. Budget ManagedAgentsBudgetLimit `json:"budget" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { @@ -38150,9 +38289,9 @@ type ManagedAgentsSession struct { Agent ManagedAgentsSessionAgent `json:"agent" api:"required"` // A timestamp in RFC 3339 format ArchivedAt time.Time `json:"archived_at" api:"required" format:"date-time"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. - Budget ManagedAgentsBudgetLimit `json:"budget" api:"required"` + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. + Budget ManagedAgentsBudgetLimit `json:"budget" api:"nullable"` // A timestamp in RFC 3339 format CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"` EnvironmentID string `json:"environment_id" api:"required"` @@ -38172,8 +38311,9 @@ type ManagedAgentsSession struct { Type ManagedAgentsSessionType `json:"type" api:"required"` // A timestamp in RFC 3339 format UpdatedAt time.Time `json:"updated_at" api:"required" format:"date-time"` - // Cumulative token usage for a session across all turns. - Usage ManagedAgentsSessionUsage `json:"usage" api:"required"` + // Cumulative Credits snapshot. Replace previous values; do not sum snapshots. + // Legacy token and list-cost fields remain available for older responses. + Usage ManagedAgentsSessionUsage `json:"usage"` // Vault IDs attached to the session at creation. Empty when no vaults were // supplied. VaultIDs []string `json:"vault_ids" api:"required"` @@ -39255,7 +39395,10 @@ Use the methods beginning with 'As' to cast the union to one of its variants. ```go type ManagedAgentsSessionEventUnion struct { - ID string `json:"id"` + ModelCredits float64 `json:"model_credits"` + SandboxRuntimeCredits float64 `json:"sandbox_runtime_credits"` + TotalCredits float64 `json:"total_credits"` + ID string `json:"id"` // This field is a union of [[]ManagedAgentsUserMessageEventContentUnion], // [[]ManagedAgentsUserCustomToolResultEventContentUnion], // [[]ManagedAgentsAgentMessageEventContentUnion], @@ -39340,6 +39483,9 @@ type ManagedAgentsSessionEventUnion struct { // This field is from variant [ManagedAgentsSessionUpdatedEvent]. Title string `json:"title"` JSON struct { + ModelCredits respjson.Field + SandboxRuntimeCredits respjson.Field + TotalCredits respjson.Field ID respjson.Field Content respjson.Field Type respjson.Field @@ -39869,6 +40015,7 @@ For type safety it is recommended to directly use a variant of the [ManagedAgent ```go type ManagedAgentsSessionEventUnionUsage struct { + Credits float64 `json:"credits"` // This field is from variant [ManagedAgentsSpanModelUsage]. CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` CacheReadInputTokens int64 `json:"cache_read_input_tokens"` @@ -39885,6 +40032,7 @@ type ManagedAgentsSessionEventUnionUsage struct { // This field is from variant [ManagedAgentsSessionUsageSnapshot]. ServerToolUse ManagedAgentsServerToolUsage `json:"server_tool_use"` JSON struct { + Credits respjson.Field CacheCreationInputTokens respjson.Field CacheReadInputTokens respjson.Field InputTokens respjson.Field @@ -42154,8 +42302,8 @@ type ManagedAgentsSessionUpdatedEvent struct { // Resolved `agent` definition for a `session`. Snapshot of the `agent` at // `session` creation time. Agent ManagedAgentsSessionAgent `json:"agent" api:"nullable"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. Budget ManagedAgentsBudgetLimit `json:"budget" api:"nullable"` // The session's full metadata bag after the update. Present when the update set // non-empty metadata; absent when metadata was unchanged or cleared to empty. @@ -42215,10 +42363,13 @@ const ( ## type [ManagedAgentsSessionUsage]() -Cumulative token usage for a session across all turns. +Cumulative Credits snapshot. Replace previous values; do not sum snapshots. Legacy token and list\-cost fields remain available for older responses. ```go type ManagedAgentsSessionUsage struct { + ModelCredits float64 `json:"model_credits"` + SandboxRuntimeCredits float64 `json:"sandbox_runtime_credits"` + TotalCredits float64 `json:"total_credits"` // Cumulative time in seconds during which the session had at least one thread in // running status. Overlapping activity from concurrent threads is counted once, // unlike `stats.active_seconds`, which sums each thread's own active time. This is @@ -42238,14 +42389,17 @@ type ManagedAgentsSessionUsage struct { ServerToolUse ManagedAgentsServerToolUsage `json:"server_tool_use" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - ActiveSeconds respjson.Field - CacheCreation respjson.Field - CacheReadInputTokens respjson.Field - InputTokens respjson.Field - ListCost respjson.Field - OutputTokens respjson.Field - ServerToolUse respjson.Field - ExtraFields map[string]respjson.Field + ModelCredits respjson.Field + SandboxRuntimeCredits respjson.Field + TotalCredits respjson.Field + ActiveSeconds respjson.Field + CacheCreation respjson.Field + CacheReadInputTokens respjson.Field + InputTokens respjson.Field + ListCost respjson.Field + OutputTokens respjson.Field + ServerToolUse respjson.Field + ExtraFields map[string]respjson.Field // contains filtered or unexported fields } `json:"-"` } @@ -42272,10 +42426,13 @@ func (r *ManagedAgentsSessionUsage) UnmarshalJSON(data []byte) error ## type [ManagedAgentsSessionUsageEvent]() -Periodic snapshot of the session's cumulative usage and tracked list cost. +Cumulative Credits emitted before session.status\_idle when usage is available. Credits are top\-level fields; Usage and Budget are legacy response fields. ```go type ManagedAgentsSessionUsageEvent struct { + ModelCredits float64 `json:"model_credits"` + SandboxRuntimeCredits float64 `json:"sandbox_runtime_credits"` + TotalCredits float64 `json:"total_credits"` // Unique identifier for this event. ID string `json:"id" api:"required"` // A timestamp in RFC 3339 format @@ -42283,18 +42440,21 @@ type ManagedAgentsSessionUsageEvent struct { // Any of "session.usage". Type ManagedAgentsSessionUsageEventType `json:"type" api:"required"` // Point-in-time snapshot of a session's cumulative usage. - Usage ManagedAgentsSessionUsageSnapshot `json:"usage" api:"required"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. + Usage ManagedAgentsSessionUsageSnapshot `json:"usage"` + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. Budget ManagedAgentsBudgetLimit `json:"budget" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - ID respjson.Field - ProcessedAt respjson.Field - Type respjson.Field - Usage respjson.Field - Budget respjson.Field - ExtraFields map[string]respjson.Field + ModelCredits respjson.Field + SandboxRuntimeCredits respjson.Field + TotalCredits respjson.Field + ID respjson.Field + ProcessedAt respjson.Field + Type respjson.Field + Usage respjson.Field + Budget respjson.Field + ExtraFields map[string]respjson.Field // contains filtered or unexported fields } `json:"-"` } @@ -42591,8 +42751,8 @@ type ManagedAgentsSpanModelRequestEndEvent struct { IsError bool `json:"is_error" api:"required"` // The id of the corresponding `span.model_request_start` event. ModelRequestStartID string `json:"model_request_start_id" api:"required"` - // Token usage for a single model request. - ModelUsage ManagedAgentsSpanModelUsage `json:"model_usage" api:"required"` + // Credits for a single model request; legacy token fields may appear in older responses. + ModelUsage ManagedAgentsSpanModelUsage `json:"model_usage"` // A timestamp in RFC 3339 format ProcessedAt time.Time `json:"processed_at" api:"required" format:"date-time"` // Any of "span.model_request_end". @@ -42708,18 +42868,19 @@ const ( ## type [ManagedAgentsSpanModelUsage]() -Token usage for a single model request. +Credits for a single model request; legacy token fields may appear in older responses. ```go type ManagedAgentsSpanModelUsage struct { + Credits float64 `json:"credits"` // Tokens used to create prompt cache in this request. - CacheCreationInputTokens int64 `json:"cache_creation_input_tokens" api:"required"` + CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` // Tokens read from prompt cache in this request. - CacheReadInputTokens int64 `json:"cache_read_input_tokens" api:"required"` + CacheReadInputTokens int64 `json:"cache_read_input_tokens"` // Input tokens consumed by this request. - InputTokens int64 `json:"input_tokens" api:"required"` + InputTokens int64 `json:"input_tokens"` // Output tokens generated by this request. - OutputTokens int64 `json:"output_tokens" api:"required"` + OutputTokens int64 `json:"output_tokens"` // Inference speed mode. `fast` provides significantly faster output token // generation at premium pricing. Not all models support `fast`; invalid // combinations are rejected at create time. @@ -42728,6 +42889,7 @@ type ManagedAgentsSpanModelUsage struct { Speed ManagedAgentsSpanModelUsageSpeed `json:"speed" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { + Credits respjson.Field CacheCreationInputTokens respjson.Field CacheReadInputTokens respjson.Field InputTokens respjson.Field @@ -42806,7 +42968,7 @@ type ManagedAgentsSpanOutcomeEvaluationEndEvent struct { Result string `json:"result" api:"required"` // Any of "span.outcome_evaluation_end". Type ManagedAgentsSpanOutcomeEvaluationEndEventType `json:"type" api:"required"` - // Token usage for a single model request. + // Credits for a single model request; legacy token fields may appear in older responses. Usage ManagedAgentsSpanModelUsage `json:"usage" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { @@ -43300,7 +43462,10 @@ Use the methods beginning with 'As' to cast the union to one of its variants. ```go type ManagedAgentsStreamSessionEventsUnion struct { - ID string `json:"id"` + ModelCredits float64 `json:"model_credits"` + SandboxRuntimeCredits float64 `json:"sandbox_runtime_credits"` + TotalCredits float64 `json:"total_credits"` + ID string `json:"id"` // This field is a union of [[]ManagedAgentsUserMessageEventContentUnion], // [[]ManagedAgentsUserCustomToolResultEventContentUnion], // [[]ManagedAgentsAgentMessageEventContentUnion], @@ -43391,6 +43556,9 @@ type ManagedAgentsStreamSessionEventsUnion struct { // This field is from variant [ManagedAgentsDeltaEvent]. EventID string `json:"event_id"` JSON struct { + ModelCredits respjson.Field + SandboxRuntimeCredits respjson.Field + TotalCredits respjson.Field ID respjson.Field Content respjson.Field Type respjson.Field @@ -43943,6 +44111,7 @@ For type safety it is recommended to directly use a variant of the [ManagedAgent ```go type ManagedAgentsStreamSessionEventsUnionUsage struct { + Credits float64 `json:"credits"` // This field is from variant [ManagedAgentsSpanModelUsage]. CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` CacheReadInputTokens int64 `json:"cache_read_input_tokens"` @@ -43959,6 +44128,7 @@ type ManagedAgentsStreamSessionEventsUnionUsage struct { // This field is from variant [ManagedAgentsSessionUsageSnapshot]. ServerToolUse ManagedAgentsServerToolUsage `json:"server_tool_use"` JSON struct { + Credits respjson.Field CacheCreationInputTokens respjson.Field CacheReadInputTokens respjson.Field InputTokens respjson.Field @@ -43993,7 +44163,10 @@ Use the methods beginning with 'As' to cast the union to one of its variants. ```go type ManagedAgentsStreamSessionThreadEventsUnion struct { - ID string `json:"id"` + ModelCredits float64 `json:"model_credits"` + SandboxRuntimeCredits float64 `json:"sandbox_runtime_credits"` + TotalCredits float64 `json:"total_credits"` + ID string `json:"id"` // This field is a union of [[]ManagedAgentsUserMessageEventContentUnion], // [[]ManagedAgentsUserCustomToolResultEventContentUnion], // [[]ManagedAgentsAgentMessageEventContentUnion], @@ -44084,6 +44257,9 @@ type ManagedAgentsStreamSessionThreadEventsUnion struct { // This field is from variant [ManagedAgentsDeltaEvent]. EventID string `json:"event_id"` JSON struct { + ModelCredits respjson.Field + SandboxRuntimeCredits respjson.Field + TotalCredits respjson.Field ID respjson.Field Content respjson.Field Type respjson.Field @@ -44636,6 +44812,7 @@ For type safety it is recommended to directly use a variant of the [ManagedAgent ```go type ManagedAgentsStreamSessionThreadEventsUnionUsage struct { + Credits float64 `json:"credits"` // This field is from variant [ManagedAgentsSpanModelUsage]. CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` CacheReadInputTokens int64 `json:"cache_read_input_tokens"` @@ -44652,6 +44829,7 @@ type ManagedAgentsStreamSessionThreadEventsUnionUsage struct { // This field is from variant [ManagedAgentsSessionUsageSnapshot]. ServerToolUse ManagedAgentsServerToolUsage `json:"server_tool_use"` JSON struct { + Credits respjson.Field CacheCreationInputTokens respjson.Field CacheReadInputTokens respjson.Field InputTokens respjson.Field @@ -52180,8 +52358,8 @@ type SessionNewParams struct { EnvironmentID string `json:"environment_id" api:"required"` // Human-readable session title. Title param.Opt[string] `json:"title,omitzero"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. Budget ManagedAgentsBudgetLimitParam `json:"budget,omitzero"` // Initial events to send to the `session` at creation, processed in order. // Supports `user.message` and `user.define_outcome` events. Maximum 50 events. @@ -53293,8 +53471,8 @@ type SessionUpdateParams struct { // updatable. Full replacement: the provided array becomes the new value. To // preserve existing entries, GET the session, modify the array, and POST it back. Agent ManagedAgentsSessionAgentUpdateParam `json:"agent,omitzero"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. Budget ManagedAgentsBudgetLimitParam `json:"budget,omitzero"` // Vault IDs (`vlt_*`) to attach to the session. Not yet supported; requests // setting this field are rejected. Reserved for future use. diff --git a/forward/channelpairing.go b/forward/channelpairing.go index b25d9a6..9ff8632 100644 --- a/forward/channelpairing.go +++ b/forward/channelpairing.go @@ -35,8 +35,10 @@ func (r *ChannelPairingService) New(ctx context.Context, params ChannelPairingNe } type ChannelPairingNewParams struct { - // Six-digit pairing code shown in the Channel message. - Code string `json:"code" api:"required"` + // Pairing code. Supply exactly one of Code or ChannelPairingID. + Code string `json:"code,omitzero"` + // Pending pairing ID. Supply exactly one of Code or ChannelPairingID. + ChannelPairingID string `json:"channel_pairing_id,omitzero"` // Forward Identity ID to bind. IdentityID string `json:"identity_id" api:"required"` // Forward Template ID to bind. @@ -86,6 +88,11 @@ func (r *DeletedChannelPairing) UnmarshalJSON(data []byte) error { } type ChannelPairing struct { + ScopeType string `json:"scope_type"` + ScopeExternalID string `json:"scope_external_id"` + ScopeDisplayName string `json:"scope_display_name"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` // Pairing ID, used when removing the pairing. ID string `json:"id"` // Always `channel_pairing`. @@ -101,15 +108,20 @@ type ChannelPairing struct { // Time the pairing completed. PairedAt string `json:"paired_at"` JSON struct { - ID respjson.Field - Type respjson.Field - ChannelID respjson.Field - IdentityID respjson.Field - TemplateID respjson.Field - Status respjson.Field - PairedAt respjson.Field - ExtraFields map[string]respjson.Field - raw string + ScopeType respjson.Field + ScopeExternalID respjson.Field + ScopeDisplayName respjson.Field + CreatedAt respjson.Field + UpdatedAt respjson.Field + ID respjson.Field + Type respjson.Field + ChannelID respjson.Field + IdentityID respjson.Field + TemplateID respjson.Field + Status respjson.Field + PairedAt respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } diff --git a/forward/environment.go b/forward/environment.go index d5b451f..541945c 100644 --- a/forward/environment.go +++ b/forward/environment.go @@ -48,6 +48,8 @@ func (r *EnvironmentService) ListAutoPaging(ctx context.Context, params Environm } type EnvironmentListParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Page size, maximum 100. Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Pagination cursor (recommended); take the value from `next_page` in the previous @@ -76,6 +78,8 @@ func (r *EnvironmentService) New(ctx context.Context, params EnvironmentNewParam } type EnvironmentNewParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Environment name; must not be empty after trimming surrounding whitespace. Name string `json:"name" api:"required"` // Description. @@ -126,6 +130,8 @@ func (r *EnvironmentService) Update(ctx context.Context, id string, params Envir } type EnvironmentUpdateParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // New name. Name param.Opt[string] `json:"name,omitzero"` // New description. @@ -239,3 +245,11 @@ func (r EnvironmentConfigPackages) RawJSON() string { return r.JSON.raw } func (r *EnvironmentConfigPackages) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } + +func (r EnvironmentNewParams) URLQuery() (url.Values, error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ArrayFormat: apiquery.ArrayQueryFormatRepeat, NestedFormat: apiquery.NestedQueryFormatBrackets}) +} + +func (r EnvironmentUpdateParams) URLQuery() (url.Values, error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ArrayFormat: apiquery.ArrayQueryFormatRepeat, NestedFormat: apiquery.NestedQueryFormatBrackets}) +} diff --git a/forward/file.go b/forward/file.go index 58e9caa..f33ca32 100644 --- a/forward/file.go +++ b/forward/file.go @@ -49,6 +49,8 @@ func (r *FileService) ListAutoPaging(ctx context.Context, params FileListParams, } type FileListParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Page size, maximum 100. Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Pagination cursor (recommended), taken from `next_page` in the previous response; @@ -83,6 +85,8 @@ func (r *FileService) Upload(ctx context.Context, params FileUploadParams, opts } type FileUploadParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Content of the file to upload. For accepted types see // [Supported upload file types](./schemas.md). File io.Reader `json:"file" api:"required" format:"binary"` @@ -183,3 +187,7 @@ type FileMetadata struct { func (r FileMetadata) RawJSON() string { return r.JSON.raw } func (r *FileMetadata) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } + +func (r FileUploadParams) URLQuery() (url.Values, error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ArrayFormat: apiquery.ArrayQueryFormatRepeat, NestedFormat: apiquery.NestedQueryFormatBrackets}) +} diff --git a/forward/identity_pairing_contract_test.go b/forward/identity_pairing_contract_test.go new file mode 100644 index 0000000..1c2cac8 --- /dev/null +++ b/forward/identity_pairing_contract_test.go @@ -0,0 +1,245 @@ +package forward_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "strings" + "testing" + + "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/option" + "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/param" + "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward" +) + +// Official Forward ownership contracts (2026-10-20), independent of inventories. +func TestIdentityOwnershipContracts(t *testing.T) { + ctx := context.Background() + for _, scope := range []struct { + name string + identity param.Opt[string] + }{{"PAT identity", forward.String("idn_ /?&")}, {"explicit empty", forward.String("")}, {"omitted", param.Opt[string]{}}} { + t.Run(scope.name, func(t *testing.T) { + identity := scope.identity + var opts []option.RequestOption + if identity.Valid() { + opts = append(opts, forward.WithIdentityID(identity.Value)) + } + cases := []struct { + method, path string + call func(forward.Client) error + }{ + {"GET", "environments", func(c forward.Client) error { + _, err := c.Environments.List(ctx, forward.EnvironmentListParams{IdentityID: identity}) + return err + }}, + {"POST", "environments", func(c forward.Client) error { + _, err := c.Environments.New(ctx, forward.EnvironmentNewParams{Name: "env", IdentityID: identity}) + return err + }}, + {"GET", "environments/env", func(c forward.Client) error { _, err := c.Environments.Get(ctx, "env", opts...); return err }}, + {"POST", "environments/env", func(c forward.Client) error { + _, err := c.Environments.Update(ctx, "env", forward.EnvironmentUpdateParams{Name: forward.String("renamed"), IdentityID: identity}) + return err + }}, + {"POST", "environments/env/archive", func(c forward.Client) error { _, err := c.Environments.Archive(ctx, "env", opts...); return err }}, + {"DELETE", "environments/env", func(c forward.Client) error { return c.Environments.Delete(ctx, "env", opts...) }}, + {"GET", "skills", func(c forward.Client) error { + _, err := c.Skills.List(ctx, forward.SkillListParams{IdentityID: identity}) + return err + }}, + {"POST", "skills", func(c forward.Client) error { + _, err := c.Skills.New(ctx, forward.SkillNewParams{Files: []io.Reader{strings.NewReader("# skill")}, IdentityID: identity}) + return err + }}, + {"GET", "skills/skill", func(c forward.Client) error { + _, err := c.Skills.Get(ctx, "skill", forward.SkillGetParams{IdentityID: identity}) + return err + }}, + {"PUT", "skills/skill", func(c forward.Client) error { + _, err := c.Skills.Update(ctx, "skill", forward.SkillUpdateParams{Description: forward.String("desc"), IdentityID: identity}) + return err + }}, + {"DELETE", "skills/skill", func(c forward.Client) error { return c.Skills.Delete(ctx, "skill", opts...) }}, + {"GET", "skills/skill/versions", func(c forward.Client) error { + _, err := c.Skills.Versions.List(ctx, "skill", forward.SkillVersionListParams{IdentityID: identity}) + return err + }}, + {"POST", "skills/skill/versions", func(c forward.Client) error { + _, err := c.Skills.Versions.New(ctx, "skill", forward.SkillVersionNewParams{Files: []io.Reader{strings.NewReader("# skill")}, IdentityID: identity}) + return err + }}, + {"GET", "skills/skill/versions/1", func(c forward.Client) error { _, err := c.Skills.Versions.Get(ctx, "skill", "1", opts...); return err }}, + {"DELETE", "skills/skill/versions/1", func(c forward.Client) error { + _, err := c.Skills.Versions.Delete(ctx, "skill", "1", opts...) + return err + }}, + {"GET", "skills/skill/versions/1/content", func(c forward.Client) error { + res, err := c.Skills.Versions.Download(ctx, "skill", "1", opts...) + if err == nil { + res.Body.Close() + } + return err + }}, + {"GET", "vaults", func(c forward.Client) error { + _, err := c.Vaults.List(ctx, forward.VaultListParams{IdentityID: identity}) + return err + }}, + {"POST", "vaults", func(c forward.Client) error { + _, err := c.Vaults.New(ctx, forward.VaultNewParams{DisplayName: "vault", IdentityID: identity}) + return err + }}, + {"GET", "vaults/vault", func(c forward.Client) error { _, err := c.Vaults.Get(ctx, "vault", opts...); return err }}, + {"DELETE", "vaults/vault", func(c forward.Client) error { return c.Vaults.Delete(ctx, "vault", opts...) }}, + {"GET", "vaults/vault/credentials", func(c forward.Client) error { + _, err := c.Vaults.Credentials.List(ctx, "vault", forward.VaultCredentialListParams{IdentityID: identity}) + return err + }}, + {"POST", "vaults/vault/credentials", func(c forward.Client) error { + _, err := c.Vaults.Credentials.New(ctx, "vault", forward.VaultCredentialNewParams{Auth: map[string]any{"type": "static_bearer", "token": "secret"}, IdentityID: identity}) + return err + }}, + {"GET", "vaults/vault/credentials/cred", func(c forward.Client) error { + _, err := c.Vaults.Credentials.Get(ctx, "vault", "cred", opts...) + return err + }}, + {"POST", "vaults/vault/credentials/cred", func(c forward.Client) error { + _, err := c.Vaults.Credentials.Update(ctx, "vault", "cred", forward.VaultCredentialUpdateParams{Metadata: map[string]any{"note": "rotation"}, IdentityID: identity}) + return err + }}, + {"DELETE", "vaults/vault/credentials/cred", func(c forward.Client) error { return c.Vaults.Credentials.Delete(ctx, "vault", "cred", opts...) }}, + {"GET", "files", func(c forward.Client) error { + _, err := c.Files.List(ctx, forward.FileListParams{IdentityID: identity}) + return err + }}, + {"POST", "files", func(c forward.Client) error { + _, err := c.Files.Upload(ctx, forward.FileUploadParams{File: strings.NewReader("content"), IdentityID: identity}) + return err + }}, + {"GET", "files/file", func(c forward.Client) error { _, err := c.Files.GetMetadata(ctx, "file", opts...); return err }}, + {"DELETE", "files/file", func(c forward.Client) error { return c.Files.Delete(ctx, "file", opts...) }}, + {"GET", "files/file/content", func(c forward.Client) error { + res, err := c.Files.Download(ctx, "file", opts...) + if err == nil { + res.Body.Close() + } + return err + }}, + } + for _, tc := range cases { + t.Run(tc.method+" "+tc.path, func(t *testing.T) { + calls := 0 + c := testClient(func(r *http.Request) (*http.Response, error) { + if r.URL.Host == "storage.test" { + if r.URL.RawQuery != "signature=ok" || r.Header.Get("Authorization") != "" { + t.Fatal("scope or auth leaked to storage") + } + return reply(r, 200, "content"), nil + } + calls++ + if r.Method != tc.method || r.URL.Path != "/api/v1/forward/"+tc.path { + t.Fatalf("route: %s %s", r.Method, r.URL) + } + query := r.URL.Query() + if query.Has("identity_id") != identity.Valid() || query.Get("identity_id") != identity.Value { + t.Fatalf("scope: %s", r.URL) + } + if len(query) > 1 { + t.Fatalf("body fields leaked to query: %s", r.URL) + } + if r.Body != nil { + body, _ := io.ReadAll(r.Body) + if strings.Contains(string(body), "identity_id") { + t.Fatalf("identity leaked into body: %s", body) + } + } + if tc.path == "files/file/content" { + return reply(r, 200, `{"url":"https://storage.test/content?signature=ok"}`), nil + } + return reply(r, 200, `{"id":"resource","data":[],"has_more":false}`), nil + }) + if err := tc.call(c); err != nil { + t.Fatal(err) + } + if calls != 1 { + t.Fatalf("API calls: %d", calls) + } + }) + } + }) + } +} + +func TestPairChannelByCodeOrID(t *testing.T) { + for _, byID := range []bool{false, true} { + t.Run(map[bool]string{false: "code", true: "ID"}[byID], func(t *testing.T) { + params := forward.ChannelPairingNewParams{IdentityID: "idn_one", TemplateID: "tmpl_one", IdempotencyKey: forward.String("pair-once")} + expected := map[string]any{"identity_id": "idn_one", "template_id": "tmpl_one"} + if byID { + params.ChannelPairingID = "pair_one" + expected["channel_pairing_id"] = "pair_one" + } else { + params.Code = "K7MP92" + expected["code"] = "K7MP92" + } + c := testClient(func(r *http.Request) (*http.Response, error) { + if r.URL.RawQuery != "" || r.Header.Get("Idempotency-Key") != "pair-once" { + t.Fatal("pairing query/header") + } + var body map[string]any + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatal(err) + } + if len(body) != len(expected) { + t.Fatalf("unexpected body: %+v", body) + } + for k, v := range expected { + if body[k] != v { + t.Fatalf("body: %+v", body) + } + } + return reply(r, 200, `{"id": "pair_one", "type": "channel_pairing", "channel_id": "channel_one", "scope_type": "room", "scope_external_id": "room_one", "scope_display_name": "Team", "identity_id": "idn_one", "template_id": "tmpl_one", "status": "active", "paired_at": "2026-10-20T01:00:00Z"}`), nil + }) + res, err := c.ChannelPairings.New(context.Background(), params) + if err != nil { + t.Fatal(err) + } + if res.ScopeType != "room" || res.ScopeExternalID != "room_one" || res.ScopeDisplayName != "Team" { + t.Fatalf("pairing=%+v", res) + } + }) + } +} + +func TestIdentityOwnershipPagingAndOverride(t *testing.T) { + calls := 0 + c := testClient(func(r *http.Request) (*http.Response, error) { + if r.URL.Query().Get("identity_id") != "idn_override" { + t.Fatalf("lost scope: %s", r.URL) + } + calls++ + if calls == 1 { + return reply(r, 200, `{"data":[{"id":"env_one"}],"has_more":true,"next_page":"next"}`), nil + } + if calls == 2 && r.URL.Query().Get("page") != "next" { + t.Fatalf("lost cursor: %s", r.URL) + } + return reply(r, 200, `{"data":[{"id":"env_two"}],"has_more":false}`), nil + }) + pager := c.Environments.ListAutoPaging(context.Background(), forward.EnvironmentListParams{IdentityID: forward.String("idn_typed")}, forward.WithIdentityID("idn_override")) + count := 0 + for pager.Next() { + count++ + } + if err := pager.Err(); err != nil { + t.Fatal(err) + } + if count != 2 { + t.Fatalf("items=%d", count) + } + _, err := c.Environments.New(context.Background(), forward.EnvironmentNewParams{Name: "env", IdentityID: forward.String("idn_typed")}, forward.WithIdentityID("idn_override")) + if err != nil { + t.Fatal(err) + } +} diff --git a/forward/params.go b/forward/params.go index e28a615..fee34ca 100644 --- a/forward/params.go +++ b/forward/params.go @@ -10,6 +10,7 @@ import ( "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention" "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/apiform" "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/apijson" + "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/option" "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/param" "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/respjson" "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/thirdparty/gjson" @@ -24,6 +25,13 @@ func Int(v int64) param.Opt[int64] { return param.NewOpt(v) } func Bool(v bool) param.Opt[bool] { return param.NewOpt(v) } func Float(v float64) param.Opt[float64] { return param.NewOpt(v) } +// WithIdentityID selects identity ownership for Forward resources using a PAT. +// Omit this option when using a SAT; its identity is inferred by the server. +// Use it on retrieval, deletion, archive, and download methods without params. +func WithIdentityID(identityID string) option.RequestOption { + return option.WithQuery("identity_id", identityID) +} + // ModelConfig preserves either a model ID string or an object from the API. // ID is populated for both shapes; RawJSON retains the original representation. type ModelConfig struct { diff --git a/forward/session.go b/forward/session.go index acb23ce..32cb425 100644 --- a/forward/session.go +++ b/forward/session.go @@ -287,11 +287,15 @@ func (r Session) RawJSON() string { return r.JSON.raw } func (r *Session) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } type SessionUsage struct { - TotalCredits float64 `json:"total_credits"` - JSON struct { - TotalCredits respjson.Field - ExtraFields map[string]respjson.Field - raw string + TotalCredits float64 `json:"total_credits"` + ModelCredits float64 `json:"model_credits"` + SandboxRuntimeCredits float64 `json:"sandbox_runtime_credits"` + JSON struct { + ModelCredits respjson.Field + SandboxRuntimeCredits respjson.Field + TotalCredits respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } diff --git a/forward/skill.go b/forward/skill.go index 8d00361..2848a45 100644 --- a/forward/skill.go +++ b/forward/skill.go @@ -50,6 +50,8 @@ func (r *SkillService) ListAutoPaging(ctx context.Context, params SkillListParam } type SkillListParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Page size, maximum 100. Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Pagination cursor (recommended), taken from `next_page` in the previous response; @@ -86,6 +88,8 @@ func (r *SkillService) New(ctx context.Context, params SkillNewParams, opts ...o } type SkillNewParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Recommended upload field, which may **repeat** several times. Two shapes are // supported: (1) a single `.zip` archive; (2) a bare file tree — each part uploads // one file and carries its relative path in `filename` (such as @@ -133,6 +137,8 @@ func (r *SkillService) Get(ctx context.Context, id string, params SkillGetParams } type SkillGetParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // ⚠️ **Deprecated**: when `true`, the response also carries `content` and // `content_encoding` (a base64 zip). Responses carry the `Deprecation: true` header // when it is used. Use [Download Skill version content](./Versions/download.md) @@ -158,6 +164,8 @@ func (r *SkillService) Update(ctx context.Context, id string, params SkillUpdate } type SkillUpdateParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // New description. Description param.Opt[string] `json:"description,omitzero"` // New content (the contents of a zip archive). Neither the archive itself nor its @@ -233,3 +241,11 @@ type Skill struct { func (r Skill) RawJSON() string { return r.JSON.raw } func (r *Skill) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } + +func (r SkillNewParams) URLQuery() (url.Values, error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ArrayFormat: apiquery.ArrayQueryFormatRepeat, NestedFormat: apiquery.NestedQueryFormatBrackets}) +} + +func (r SkillUpdateParams) URLQuery() (url.Values, error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ArrayFormat: apiquery.ArrayQueryFormatRepeat, NestedFormat: apiquery.NestedQueryFormatBrackets}) +} diff --git a/forward/skillversion.go b/forward/skillversion.go index f0aedec..58a5fc9 100644 --- a/forward/skillversion.go +++ b/forward/skillversion.go @@ -52,6 +52,8 @@ func (r *SkillVersionService) ListAutoPaging(ctx context.Context, id string, par } type SkillVersionListParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Page size, maximum 100, default 20. Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Cursor for paging forward; take the value from `next_page` in the previous @@ -77,6 +79,8 @@ func (r *SkillVersionService) New(ctx context.Context, id string, params SkillVe } type SkillVersionNewParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Upload field, which may appear **multiple** times. Two shapes are supported: • a // single `.zip` archive; • a bare file tree, where every part uploads one file and // `filename` carries its relative path (e.g. `customer-reply/SKILL.md`, @@ -185,3 +189,7 @@ type SkillVersion struct { func (r SkillVersion) RawJSON() string { return r.JSON.raw } func (r *SkillVersion) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } + +func (r SkillVersionNewParams) URLQuery() (url.Values, error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ArrayFormat: apiquery.ArrayQueryFormatRepeat, NestedFormat: apiquery.NestedQueryFormatBrackets}) +} diff --git a/forward/vault.go b/forward/vault.go index 00537f1..928948c 100644 --- a/forward/vault.go +++ b/forward/vault.go @@ -49,6 +49,8 @@ func (r *VaultService) ListAutoPaging(ctx context.Context, params VaultListParam } type VaultListParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Page size, maximum 100. Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Pagination cursor (recommended), taken from `next_page` in the previous @@ -79,6 +81,8 @@ func (r *VaultService) New(ctx context.Context, params VaultNewParams, opts ...o } type VaultNewParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Vault display name. DisplayName string `json:"display_name" api:"required"` // Metadata object; `created_by` is reserved and cannot be supplied (supplying it @@ -150,3 +154,7 @@ type Vault struct { func (r Vault) RawJSON() string { return r.JSON.raw } func (r *Vault) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } + +func (r VaultNewParams) URLQuery() (url.Values, error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ArrayFormat: apiquery.ArrayQueryFormatRepeat, NestedFormat: apiquery.NestedQueryFormatBrackets}) +} diff --git a/forward/vaultcredential.go b/forward/vaultcredential.go index c9c453f..0091ec3 100644 --- a/forward/vaultcredential.go +++ b/forward/vaultcredential.go @@ -51,6 +51,8 @@ func (r *VaultCredentialService) ListAutoPaging(ctx context.Context, id string, } type VaultCredentialListParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Page size, maximum 100. Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Pagination cursor (recommended), taken from `next_page` in the previous response; @@ -84,6 +86,8 @@ func (r *VaultCredentialService) New(ctx context.Context, id string, params Vaul } type VaultCredentialNewParams struct { + // Select identity ownership with a PAT. Omit when using a SAT. + IdentityID param.Opt[string] `query:"identity_id,omitzero" json:"-"` // Credential authentication material; supports `static_bearer` and `mcp_oauth`. The // response only returns redacted, non-secret fields. Auth map[string]any `json:"auth" api:"required"` @@ -293,3 +297,7 @@ func (r MCPOAuthRefreshUpdateParam) MarshalJSON() ([]byte, error) { func (r *MCPOAuthRefreshUpdateParam) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } + +func (r VaultCredentialNewParams) URLQuery() (url.Values, error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ArrayFormat: apiquery.ArrayQueryFormatRepeat, NestedFormat: apiquery.NestedQueryFormatBrackets}) +} diff --git a/managed/deployment.go b/managed/deployment.go index cc2c345..71e998b 100644 --- a/managed/deployment.go +++ b/managed/deployment.go @@ -243,8 +243,8 @@ type ManagedAgentsDeployment struct { // Vault IDs supplying stored credentials for sessions created from this // deployment. VaultIDs []string `json:"vault_ids" api:"required"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. Budget ManagedAgentsBudgetLimit `json:"budget" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { @@ -1848,8 +1848,8 @@ type DeploymentNewParams struct { Name string `json:"name" api:"required"` // Description of what the deployment does. Description param.Opt[string] `json:"description,omitzero"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. Budget ManagedAgentsBudgetLimitParam `json:"budget,omitzero"` // Arbitrary key-value metadata. Maximum 16 pairs, keys up to 64 chars, values up // to 512 chars. @@ -2045,8 +2045,8 @@ type DeploymentUpdateParams struct { // version, or an `agent` object with both id and version specified. Omit to // preserve. Cannot be cleared. Agent DeploymentUpdateParamsAgentUnion `json:"agent,omitzero"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. Budget ManagedAgentsBudgetLimitParam `json:"budget,omitzero"` // Initial events. Full replacement. Omit to preserve. Cannot be cleared. At least // 1, maximum 50. diff --git a/managed/qoder_types_test.go b/managed/qoder_types_test.go index 0137ab4..1bc9df1 100644 --- a/managed/qoder_types_test.go +++ b/managed/qoder_types_test.go @@ -67,6 +67,11 @@ func TestQoderFieldAlignment(t *testing.T) { } for name, fields := range expected { for _, f := range fields { + // QCA uses decimal Credits, not the upstream monetary budget. + // https://docs.qoder.com/cloud-agents/api/sessions/schemas#session-budget + if (name == "ManagedAgentsBudgetLimit" || name == "ManagedAgentsBudgetLimitParam") && f == "MaxListCost" { + f = "MaxCreditCost" + } if !actual[name][f] { t.Errorf("upstream field missing: %s.%s", name, f) } diff --git a/managed/session.go b/managed/session.go index 29d7604..9064603 100644 --- a/managed/session.go +++ b/managed/session.go @@ -540,19 +540,19 @@ func (r *ManagedAgentsBranchCheckoutParam) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// A hard spend ceiling. The session stops issuing new model requests once the -// tracked list cost reaches `max_list_cost`. +// A total model and sandbox credit limit. Once reached, new user messages are rejected; +// an already running turn continues to completion. type ManagedAgentsBudgetLimit struct { - // A monetary amount in a specific currency. - MaxListCost MonetaryAmount `json:"max_list_cost" api:"required"` + // Positive decimal Credits, with at most 12 integer and 2 fractional digits. + MaxCreditCost string `json:"max_credit_cost" api:"required"` // Any of "limit". Type ManagedAgentsBudgetLimitType `json:"type" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - MaxListCost respjson.Field - Type respjson.Field - ExtraFields map[string]respjson.Field - raw string + MaxCreditCost respjson.Field + Type respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } @@ -578,13 +578,13 @@ const ( ManagedAgentsBudgetLimitTypeLimit ManagedAgentsBudgetLimitType = "limit" ) -// A hard spend ceiling. The session stops issuing new model requests once the -// tracked list cost reaches `max_list_cost`. +// A total model and sandbox credit limit. Once reached, new user messages are rejected; +// an already running turn continues to completion. // -// The properties MaxListCost, Type are required. +// The properties MaxCreditCost, Type are required. type ManagedAgentsBudgetLimitParam struct { - // A monetary amount in a specific currency. - MaxListCost MonetaryAmountParam `json:"max_list_cost,omitzero" api:"required"` + // Positive decimal Credits, with at most 12 integer and 2 fractional digits. + MaxCreditCost string `json:"max_credit_cost" api:"required"` // Any of "limit". Type ManagedAgentsBudgetLimitType `json:"type,omitzero" api:"required"` paramObj @@ -1226,9 +1226,9 @@ type ManagedAgentsSession struct { Agent ManagedAgentsSessionAgent `json:"agent" api:"required"` // A timestamp in RFC 3339 format ArchivedAt time.Time `json:"archived_at" api:"required" format:"date-time"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. - Budget ManagedAgentsBudgetLimit `json:"budget" api:"required"` + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. + Budget ManagedAgentsBudgetLimit `json:"budget" api:"nullable"` // A timestamp in RFC 3339 format CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"` EnvironmentID string `json:"environment_id" api:"required"` @@ -1248,8 +1248,9 @@ type ManagedAgentsSession struct { Type ManagedAgentsSessionType `json:"type" api:"required"` // A timestamp in RFC 3339 format UpdatedAt time.Time `json:"updated_at" api:"required" format:"date-time"` - // Cumulative token usage for a session across all turns. - Usage ManagedAgentsSessionUsage `json:"usage" api:"required"` + // Cumulative Credits snapshot. Replace previous values; do not sum snapshots. + // Legacy token and list-cost fields remain available for older responses. + Usage ManagedAgentsSessionUsage `json:"usage"` // Vault IDs attached to the session at creation. Empty when no vaults were // supplied. VaultIDs []string `json:"vault_ids" api:"required"` @@ -1983,8 +1984,8 @@ type ManagedAgentsSessionUpdatedEvent struct { // Resolved `agent` definition for a `session`. Snapshot of the `agent` at // `session` creation time. Agent ManagedAgentsSessionAgent `json:"agent" api:"nullable"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. Budget ManagedAgentsBudgetLimit `json:"budget" api:"nullable"` // The session's full metadata bag after the update. Present when the update set // non-empty metadata; absent when metadata was unchanged or cleared to empty. @@ -2017,8 +2018,12 @@ const ( ManagedAgentsSessionUpdatedEventTypeSessionUpdated ManagedAgentsSessionUpdatedEventType = "session.updated" ) -// Cumulative token usage for a session across all turns. +// Cumulative Credits snapshot. Replace previous values; do not sum snapshots. +// Legacy token and list-cost fields remain available for older responses. type ManagedAgentsSessionUsage struct { + ModelCredits float64 `json:"model_credits"` + SandboxRuntimeCredits float64 `json:"sandbox_runtime_credits"` + TotalCredits float64 `json:"total_credits"` // Cumulative time in seconds during which the session had at least one thread in // running status. Overlapping activity from concurrent threads is counted once, // unlike `stats.active_seconds`, which sums each thread's own active time. This is @@ -2038,15 +2043,18 @@ type ManagedAgentsSessionUsage struct { ServerToolUse ManagedAgentsServerToolUsage `json:"server_tool_use" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - ActiveSeconds respjson.Field - CacheCreation respjson.Field - CacheReadInputTokens respjson.Field - InputTokens respjson.Field - ListCost respjson.Field - OutputTokens respjson.Field - ServerToolUse respjson.Field - ExtraFields map[string]respjson.Field - raw string + ModelCredits respjson.Field + SandboxRuntimeCredits respjson.Field + TotalCredits respjson.Field + ActiveSeconds respjson.Field + CacheCreation respjson.Field + CacheReadInputTokens respjson.Field + InputTokens respjson.Field + ListCost respjson.Field + OutputTokens respjson.Field + ServerToolUse respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } @@ -2056,8 +2064,12 @@ func (r *ManagedAgentsSessionUsage) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// Periodic snapshot of the session's cumulative usage and tracked list cost. +// Cumulative Credits emitted before session.status_idle when usage is available. +// Credits are top-level fields; Usage and Budget are legacy response fields. type ManagedAgentsSessionUsageEvent struct { + ModelCredits float64 `json:"model_credits"` + SandboxRuntimeCredits float64 `json:"sandbox_runtime_credits"` + TotalCredits float64 `json:"total_credits"` // Unique identifier for this event. ID string `json:"id" api:"required"` // A timestamp in RFC 3339 format @@ -2065,19 +2077,22 @@ type ManagedAgentsSessionUsageEvent struct { // Any of "session.usage". Type ManagedAgentsSessionUsageEventType `json:"type" api:"required"` // Point-in-time snapshot of a session's cumulative usage. - Usage ManagedAgentsSessionUsageSnapshot `json:"usage" api:"required"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. + Usage ManagedAgentsSessionUsageSnapshot `json:"usage"` + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. Budget ManagedAgentsBudgetLimit `json:"budget" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - ID respjson.Field - ProcessedAt respjson.Field - Type respjson.Field - Usage respjson.Field - Budget respjson.Field - ExtraFields map[string]respjson.Field - raw string + ModelCredits respjson.Field + SandboxRuntimeCredits respjson.Field + TotalCredits respjson.Field + ID respjson.Field + ProcessedAt respjson.Field + Type respjson.Field + Usage respjson.Field + Budget respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } @@ -2472,8 +2487,8 @@ type SessionNewParams struct { EnvironmentID string `json:"environment_id" api:"required"` // Human-readable session title. Title param.Opt[string] `json:"title,omitzero"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. Budget ManagedAgentsBudgetLimitParam `json:"budget,omitzero"` // Initial events to send to the `session` at creation, processed in order. // Supports `user.message` and `user.define_outcome` events. Maximum 50 events. @@ -2804,8 +2819,8 @@ type SessionUpdateParams struct { // updatable. Full replacement: the provided array becomes the new value. To // preserve existing entries, GET the session, modify the array, and POST it back. Agent ManagedAgentsSessionAgentUpdateParam `json:"agent,omitzero"` - // A hard spend ceiling. The session stops issuing new model requests once the - // tracked list cost reaches `max_list_cost`. + // A total model and sandbox credit limit. Once reached, new user messages are + // rejected; an already running turn continues to completion. Budget ManagedAgentsBudgetLimitParam `json:"budget,omitzero"` // Vault IDs (`vlt_*`) to attach to the session. Not yet supported; requests // setting this field are rejected. Reserved for future use. diff --git a/managed/session_credit_contract_test.go b/managed/session_credit_contract_test.go new file mode 100644 index 0000000..17b9790 --- /dev/null +++ b/managed/session_credit_contract_test.go @@ -0,0 +1,124 @@ +package managed_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "reflect" + "strings" + "testing" + + "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/param" + "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward" + "github.com/QoderAI/qoder-cloud-agents-sdk-go/managed" +) + +// Contract: https://docs.qoder.com/cloud-agents/api/sessions/schemas (2026-10-20). +func TestSessionCreditBudgetWireContract(t *testing.T) { + for _, tc := range []struct { + name string + params managed.SessionUpdateParams + want string + }{ + {"decimal", managed.SessionUpdateParams{Budget: managed.ManagedAgentsBudgetLimitParam{Type: "limit", MaxCreditCost: "100.00"}}, `{"budget":{"type":"limit","max_credit_cost":"100.00"}}`}, + {"omit", managed.SessionUpdateParams{}, `{}`}, + {"clear", managed.SessionUpdateParams{Budget: param.NullStruct[managed.ManagedAgentsBudgetLimitParam]()}, `{"budget":null}`}, + } { + t.Run(tc.name, func(t *testing.T) { + client := testClient(func(r *http.Request) (*http.Response, error) { + body, _ := io.ReadAll(r.Body) + var got, want any + if err := json.Unmarshal(body, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(tc.want), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("got %s want %s", body, tc.want) + } + return reply(r, 200, `{"id":"sess_one","budget":{"type":"limit","max_credit_cost":"100.00"}}`), nil + }) + session, err := client.Sessions.Update(context.Background(), "sess_one", tc.params) + if err != nil { + t.Fatal(err) + } + if session.Budget.MaxCreditCost != "100.00" { + t.Fatalf("budget=%+v", session.Budget) + } + }) + } +} + +func TestSessionCreditsSnapshotsAndEvents(t *testing.T) { + const usage = `{"model_credits":7.66,"sandbox_runtime_credits":0.34,"total_credits":8}` + var session managed.ManagedAgentsSession + if err := json.Unmarshal([]byte(`{"id":"sess_one","usage":`+usage+`}`), &session); err != nil { + t.Fatal(err) + } + if session.Usage.ModelCredits != 7.66 || session.Usage.SandboxRuntimeCredits != 0.34 || session.Usage.TotalCredits != 8 { + t.Fatalf("usage=%+v", session.Usage) + } + var fsession forward.Session + if err := json.Unmarshal([]byte(`{"id":"sess_one","usage":`+usage+`}`), &fsession); err != nil { + t.Fatal(err) + } + if fsession.Usage.ModelCredits != 7.66 || fsession.Usage.SandboxRuntimeCredits != 0.34 || fsession.Usage.TotalCredits != 8 { + t.Fatalf("forward usage=%+v", fsession.Usage) + } + var empty managed.ManagedAgentsSession + if err := json.Unmarshal([]byte(`{"id":"sess_empty"}`), &empty); err != nil { + t.Fatal(err) + } + if empty.JSON.Usage.Valid() { + t.Fatal("missing usage must stay absent") + } + const event = `{"id":"evt_one","type":"session.usage","model_credits":7.66,"sandbox_runtime_credits":0.34,"total_credits":8}` + var history managed.ManagedAgentsSessionEventUnion + if err := json.Unmarshal([]byte(event), &history); err != nil { + t.Fatal(err) + } + if history.TotalCredits != 8 || history.AsSessionUsage().TotalCredits != 8 { + t.Fatal("history lost top-level credits") + } + var thread managed.ManagedAgentsStreamSessionThreadEventsUnion + if err := json.Unmarshal([]byte(event), &thread); err != nil { + t.Fatal(err) + } + if thread.TotalCredits != 8 || thread.AsSessionUsage().ModelCredits != 7.66 { + t.Fatal("thread stream lost credits") + } + client := testClient(func(r *http.Request) (*http.Response, error) { + res := reply(r, 200, "id: evt_one\nevent: session.usage\ndata: "+event+"\n\nid: evt_two\nevent: agent.thread_context_compacted\ndata: {\"id\":\"evt_two\",\"type\":\"agent.thread_context_compacted\",\"usage\":{\"credits\":0}}\n\nid: evt_three\nevent: span.model_request_end\ndata: {\"id\":\"evt_three\",\"type\":\"span.model_request_end\",\"model_usage\":{\"credits\":1.25}}\n\n") + res.Header.Set("Content-Type", "text/event-stream") + return res, nil + }) + stream := client.Sessions.Events.StreamEvents(context.Background(), "sess_one", managed.SessionEventStreamParams{}) + defer stream.Close() + var seen []string + for stream.Next() { + e := stream.Current() + seen = append(seen, e.Type) + switch e.Type { + case "session.usage": + if e.TotalCredits != 8 || e.AsSessionUsage().TotalCredits != 8 { + t.Fatal("SSE lost credits") + } + case "agent.thread_context_compacted": + if !e.Usage.JSON.Credits.Valid() || e.AsAgentThreadContextCompacted().Usage.Credits != 0 { + t.Fatal("zero credits must be preserved") + } + case "span.model_request_end": + if e.ModelUsage.Credits != 1.25 { + t.Fatal("model credits lost") + } + } + } + if err := stream.Err(); err != nil { + t.Fatal(err) + } + if strings.Join(seen, ",") != "session.usage,agent.thread_context_compacted,span.model_request_end" { + t.Fatal(seen) + } +} diff --git a/managed/sessionevent.go b/managed/sessionevent.go index 3c82925..d9a1e48 100644 --- a/managed/sessionevent.go +++ b/managed/sessionevent.go @@ -513,6 +513,8 @@ const ( // Indicates that context compaction (summarization) occurred during the session. type ManagedAgentsAgentThreadContextCompactedEvent struct { + // Credits for this compaction call, already included in Session totals. + Usage ManagedAgentsSpanModelUsage `json:"usage"` // Unique identifier for this event. ID string `json:"id" api:"required"` // A timestamp in RFC 3339 format @@ -521,6 +523,7 @@ type ManagedAgentsAgentThreadContextCompactedEvent struct { Type ManagedAgentsAgentThreadContextCompactedEventType `json:"type" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { + Usage respjson.Field ID respjson.Field ProcessedAt respjson.Field Type respjson.Field @@ -3757,7 +3760,10 @@ const ( // // Use the methods beginning with 'As' to cast the union to one of its variants. type ManagedAgentsSessionEventUnion struct { - ID string `json:"id"` + ModelCredits float64 `json:"model_credits"` + SandboxRuntimeCredits float64 `json:"sandbox_runtime_credits"` + TotalCredits float64 `json:"total_credits"` + ID string `json:"id"` // This field is a union of [[]ManagedAgentsUserMessageEventContentUnion], // [[]ManagedAgentsUserCustomToolResultEventContentUnion], // [[]ManagedAgentsAgentMessageEventContentUnion], @@ -3842,6 +3848,9 @@ type ManagedAgentsSessionEventUnion struct { // This field is from variant [ManagedAgentsSessionUpdatedEvent]. Title string `json:"title"` JSON struct { + ModelCredits respjson.Field + SandboxRuntimeCredits respjson.Field + TotalCredits respjson.Field ID respjson.Field Content respjson.Field Type respjson.Field @@ -4321,6 +4330,7 @@ func (r *ManagedAgentsSessionEventUnionStopReason) UnmarshalJSON(data []byte) er // For type safety it is recommended to directly use a variant of the // [ManagedAgentsSessionEventUnion]. type ManagedAgentsSessionEventUnionUsage struct { + Credits float64 `json:"credits"` // This field is from variant [ManagedAgentsSpanModelUsage]. CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` CacheReadInputTokens int64 `json:"cache_read_input_tokens"` @@ -4337,6 +4347,7 @@ type ManagedAgentsSessionEventUnionUsage struct { // This field is from variant [ManagedAgentsSessionUsageSnapshot]. ServerToolUse ManagedAgentsServerToolUsage `json:"server_tool_use"` JSON struct { + Credits respjson.Field CacheCreationInputTokens respjson.Field CacheReadInputTokens respjson.Field InputTokens respjson.Field @@ -4949,8 +4960,8 @@ type ManagedAgentsSpanModelRequestEndEvent struct { IsError bool `json:"is_error" api:"required"` // The id of the corresponding `span.model_request_start` event. ModelRequestStartID string `json:"model_request_start_id" api:"required"` - // Token usage for a single model request. - ModelUsage ManagedAgentsSpanModelUsage `json:"model_usage" api:"required"` + // Credits for a single model request; legacy token fields may appear in older responses. + ModelUsage ManagedAgentsSpanModelUsage `json:"model_usage"` // A timestamp in RFC 3339 format ProcessedAt time.Time `json:"processed_at" api:"required" format:"date-time"` // Any of "span.model_request_end". @@ -5010,16 +5021,17 @@ const ( ManagedAgentsSpanModelRequestStartEventTypeSpanModelRequestStart ManagedAgentsSpanModelRequestStartEventType = "span.model_request_start" ) -// Token usage for a single model request. +// Credits for a single model request; legacy token fields may appear in older responses. type ManagedAgentsSpanModelUsage struct { + Credits float64 `json:"credits"` // Tokens used to create prompt cache in this request. - CacheCreationInputTokens int64 `json:"cache_creation_input_tokens" api:"required"` + CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` // Tokens read from prompt cache in this request. - CacheReadInputTokens int64 `json:"cache_read_input_tokens" api:"required"` + CacheReadInputTokens int64 `json:"cache_read_input_tokens"` // Input tokens consumed by this request. - InputTokens int64 `json:"input_tokens" api:"required"` + InputTokens int64 `json:"input_tokens"` // Output tokens generated by this request. - OutputTokens int64 `json:"output_tokens" api:"required"` + OutputTokens int64 `json:"output_tokens"` // Inference speed mode. `fast` provides significantly faster output token // generation at premium pricing. Not all models support `fast`; invalid // combinations are rejected at create time. @@ -5028,6 +5040,7 @@ type ManagedAgentsSpanModelUsage struct { Speed ManagedAgentsSpanModelUsageSpeed `json:"speed" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { + Credits respjson.Field CacheCreationInputTokens respjson.Field CacheReadInputTokens respjson.Field InputTokens respjson.Field @@ -5083,7 +5096,7 @@ type ManagedAgentsSpanOutcomeEvaluationEndEvent struct { Result string `json:"result" api:"required"` // Any of "span.outcome_evaluation_end". Type ManagedAgentsSpanOutcomeEvaluationEndEventType `json:"type" api:"required"` - // Token usage for a single model request. + // Credits for a single model request; legacy token fields may appear in older responses. Usage ManagedAgentsSpanModelUsage `json:"usage" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { @@ -5230,7 +5243,10 @@ const ( // // Use the methods beginning with 'As' to cast the union to one of its variants. type ManagedAgentsStreamSessionEventsUnion struct { - ID string `json:"id"` + ModelCredits float64 `json:"model_credits"` + SandboxRuntimeCredits float64 `json:"sandbox_runtime_credits"` + TotalCredits float64 `json:"total_credits"` + ID string `json:"id"` // This field is a union of [[]ManagedAgentsUserMessageEventContentUnion], // [[]ManagedAgentsUserCustomToolResultEventContentUnion], // [[]ManagedAgentsAgentMessageEventContentUnion], @@ -5321,6 +5337,9 @@ type ManagedAgentsStreamSessionEventsUnion struct { // This field is from variant [ManagedAgentsDeltaEvent]. EventID string `json:"event_id"` JSON struct { + ModelCredits respjson.Field + SandboxRuntimeCredits respjson.Field + TotalCredits respjson.Field ID respjson.Field Content respjson.Field Type respjson.Field @@ -5833,6 +5852,7 @@ func (r *ManagedAgentsStreamSessionEventsUnionStopReason) UnmarshalJSON(data []b // For type safety it is recommended to directly use a variant of the // [ManagedAgentsStreamSessionEventsUnion]. type ManagedAgentsStreamSessionEventsUnionUsage struct { + Credits float64 `json:"credits"` // This field is from variant [ManagedAgentsSpanModelUsage]. CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` CacheReadInputTokens int64 `json:"cache_read_input_tokens"` @@ -5849,6 +5869,7 @@ type ManagedAgentsStreamSessionEventsUnionUsage struct { // This field is from variant [ManagedAgentsSessionUsageSnapshot]. ServerToolUse ManagedAgentsServerToolUsage `json:"server_tool_use"` JSON struct { + Credits respjson.Field CacheCreationInputTokens respjson.Field CacheReadInputTokens respjson.Field InputTokens respjson.Field diff --git a/managed/sessionthread.go b/managed/sessionthread.go index d22c0b3..9e5ac57 100644 --- a/managed/sessionthread.go +++ b/managed/sessionthread.go @@ -407,7 +407,10 @@ func (r *ManagedAgentsSessionThreadUsage) UnmarshalJSON(data []byte) error { // // Use the methods beginning with 'As' to cast the union to one of its variants. type ManagedAgentsStreamSessionThreadEventsUnion struct { - ID string `json:"id"` + ModelCredits float64 `json:"model_credits"` + SandboxRuntimeCredits float64 `json:"sandbox_runtime_credits"` + TotalCredits float64 `json:"total_credits"` + ID string `json:"id"` // This field is a union of [[]ManagedAgentsUserMessageEventContentUnion], // [[]ManagedAgentsUserCustomToolResultEventContentUnion], // [[]ManagedAgentsAgentMessageEventContentUnion], @@ -498,6 +501,9 @@ type ManagedAgentsStreamSessionThreadEventsUnion struct { // This field is from variant [ManagedAgentsDeltaEvent]. EventID string `json:"event_id"` JSON struct { + ModelCredits respjson.Field + SandboxRuntimeCredits respjson.Field + TotalCredits respjson.Field ID respjson.Field Content respjson.Field Type respjson.Field @@ -1021,6 +1027,7 @@ func (r *ManagedAgentsStreamSessionThreadEventsUnionStopReason) UnmarshalJSON(da // For type safety it is recommended to directly use a variant of the // [ManagedAgentsStreamSessionThreadEventsUnion]. type ManagedAgentsStreamSessionThreadEventsUnionUsage struct { + Credits float64 `json:"credits"` // This field is from variant [ManagedAgentsSpanModelUsage]. CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` CacheReadInputTokens int64 `json:"cache_read_input_tokens"` @@ -1037,6 +1044,7 @@ type ManagedAgentsStreamSessionThreadEventsUnionUsage struct { // This field is from variant [ManagedAgentsSessionUsageSnapshot]. ServerToolUse ManagedAgentsServerToolUsage `json:"server_tool_use"` JSON struct { + Credits respjson.Field CacheCreationInputTokens respjson.Field CacheReadInputTokens respjson.Field InputTokens respjson.Field