Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions providers/openai/language_model_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ func DefaultUsageFunc(response openai.ChatCompletion) (fantasy.Usage, fantasy.Pr
}
// OpenAI reports prompt_tokens INCLUDING cached tokens. Subtract to avoid double-counting.
inputTokens := max(response.Usage.PromptTokens-promptTokenDetails.CachedTokens, 0)
providerMetadata.ExtraFields = ExtractExtraFields(response.Usage.JSON.ExtraFields)
return fantasy.Usage{
InputTokens: inputTokens,
OutputTokens: response.Usage.CompletionTokens,
Expand Down Expand Up @@ -263,6 +264,8 @@ func DefaultStreamUsageFunc(chunk openai.ChatCompletionChunk, _ map[string]any,
}
}

streamProviderMetadata.ExtraFields = ExtractExtraFields(chunk.Usage.JSON.ExtraFields)

return usage, fantasy.ProviderMetadata{
Name: streamProviderMetadata,
}
Expand Down
35 changes: 35 additions & 0 deletions providers/openai/provider_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"charm.land/fantasy"
"github.com/charmbracelet/openai-go"
"github.com/charmbracelet/openai-go/packages/respjson"
)

// ReasoningEffort represents the reasoning effort level for OpenAI models.
Expand Down Expand Up @@ -63,6 +64,40 @@ type ProviderMetadata struct {
Logprobs []openai.ChatCompletionTokenLogprob `json:"logprobs"`
AcceptedPredictionTokens int64 `json:"accepted_prediction_tokens"`
RejectedPredictionTokens int64 `json:"rejected_prediction_tokens"`
// ExtraFields captures non-standard fields from the usage object.
// Keys are field names, values are raw JSON.
ExtraFields map[string]json.RawMessage `json:"extra_fields,omitempty"`
}

// ExtraField parses an extra usage field into the provided target.
// Returns false if the field is not present or cannot be parsed.
func (m *ProviderMetadata) ExtraField(key string, target any) bool {
if m == nil || m.ExtraFields == nil {
return false
}
raw, ok := m.ExtraFields[key]
if !ok {
return false
}
return json.Unmarshal(raw, target) == nil
}

// ExtractExtraFields reads non-standard fields from the SDK's
// ExtraFields map and returns them as a map of raw JSON values.
func ExtractExtraFields(extraFields map[string]respjson.Field) map[string]json.RawMessage {
if len(extraFields) == 0 {
return nil
}
ext := make(map[string]json.RawMessage, len(extraFields))
for k, f := range extraFields {
if raw := f.Raw(); raw != "" && raw != "null" {
ext[k] = json.RawMessage(raw)
}
}
if len(ext) == 0 {
return nil
}
return ext
}

// Options implements the ProviderOptions interface.
Expand Down
9 changes: 9 additions & 0 deletions providers/openaicompat/openaicompat.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,12 @@ func WithResponsesAPIFunc(fn func(modelID string) bool) Option {
o.openaiOptions = append(o.openaiOptions, openai.WithResponsesAPIFunc(fn))
}
}

// WithLanguageModelOptions appends language model options to the provider.
// This allows callers to customize usage extraction, stream handling, and
// other language model behaviors.
func WithLanguageModelOptions(opts ...openai.LanguageModelOption) Option {
return func(o *options) {
o.languageModelOptions = append(o.languageModelOptions, opts...)
}
}
Loading