Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import AppKit
import CodexBarCore
import Foundation

struct SakanaProviderImplementation: ProviderImplementation {
let id: UsageProvider = .sakana

@MainActor
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
ProviderPresentation { _ in "web" }
}

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.sakanaCookieHeader
}

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
SakanaSettingsReader.cookieHeader(environment: context.environment) != nil
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
let subtitle = "Stored in ~/.codexbar/config.json. Copy the Sakana AI console Cookie request header."

return [
ProviderSettingsFieldDescriptor(
id: "sakana-cookie",
title: "Cookie header",
subtitle: subtitle,
kind: .secure,
placeholder: "Cookie: ...",
binding: context.stringBinding(\.sakanaCookieHeader),
actions: [
ProviderSettingsActionDescriptor(
id: "sakana-open-dashboard",
title: "Open Sakana AI Console",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://console.sakana.ai/billing") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: nil,
onActivate: nil),
]
}
}
14 changes: 14 additions & 0 deletions Sources/CodexBar/Providers/Sakana/SakanaSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var sakanaCookieHeader: String {
get { self.configSnapshot.providerConfig(for: .sakana)?.sanitizedCookieHeader ?? "" }
set {
self.updateProviderConfig(provider: .sakana) { entry in
entry.cookieHeader = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .sakana, field: "cookieHeader", value: newValue)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ enum ProviderImplementationRegistry {
case .perplexity: PerplexityProviderImplementation()
case .mimo: MiMoProviderImplementation()
case .doubao: DoubaoProviderImplementation()
case .sakana: SakanaProviderImplementation()
case .abacus: AbacusProviderImplementation()
case .mistral: MistralProviderImplementation()
case .deepseek: DeepSeekProviderImplementation()
Expand Down
5 changes: 5 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-sakana.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,7 @@ extension UsageStore {
.jetbrains: "JetBrains AI debug log not yet implemented",
.mimo: "Xiaomi MiMo debug log not yet implemented",
.doubao: "Doubao debug log not yet implemented",
.sakana: "Sakana AI debug log not yet implemented",
.venice: "Venice debug log not yet implemented",
.commandcode: "Command Code debug log not yet implemented",
.stepfun: "StepFun debug log not yet implemented",
Expand Down Expand Up @@ -1085,8 +1086,8 @@ extension UsageStore {
hasTokenAccount: deepSeekHasTokenAccount)
case .gemini, .antigravity, .opencode, .opencodego, .alibabatokenplan, .factory, .copilot, .devin,
.vertexai, .kilo, .kiro, .kimi, .kimik2, .moonshot, .jetbrains, .perplexity, .mimo, .doubao,
.abacus, .mistral, .codebuff, .crof, .windsurf, .venice, .manus, .commandcode, .stepfun, .bedrock,
.grok, .groq, .t3chat, .llmproxy, .litellm, .zed, .deepgram, .poe, .chutes:
.sakana, .abacus, .mistral, .codebuff, .crof, .windsurf, .venice, .manus, .commandcode, .stepfun,
.bedrock, .grok, .groq, .t3chat, .llmproxy, .litellm, .zed, .deepgram, .poe, .chutes:
return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
}
}
Expand Down
6 changes: 6 additions & 0 deletions Sources/CodexBarCLI/CLIUsageCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,12 @@ extension CodexBarCLI {
{
return false
}
if provider == .sakana,
sourceMode == .auto || sourceMode == .web,
environment.map({ SakanaSettingsReader.cookieHeader(environment: $0) != nil }) == true
{
return false
}
if provider == .ollama,
sourceMode == .auto
{
Expand Down
14 changes: 14 additions & 0 deletions Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ public enum ProviderConfigEnvironment {
self.applyAzureOpenAIOverrides(base: base, config: config)
case .kimi:
self.applyKimiOverrides(base: base, config: config)
case .sakana:
self.applySakanaOverrides(base: base, config: config)
default:
nil
}
Expand Down Expand Up @@ -285,6 +287,18 @@ public enum ProviderConfigEnvironment {
return env
}

private static func applySakanaOverrides(
base: [String: String],
config: ProviderConfig?) -> [String: String]
{
guard let config else { return base }
var env = base
if let cookieHeader = config.sanitizedCookieHeader {
env[SakanaSettingsReader.cookieHeaderKey] = cookieHeader
}
return env
}

private static func applyAzureOpenAIOverrides(
base: [String: String],
config: ProviderConfig?) -> [String: String]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.

enum CodexParserHash {
static let value = "800a06dead603ea7"
static let value = "e59cf52c58111c43"
}
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Providers/ProviderDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public enum ProviderDescriptorRegistry {
.perplexity: PerplexityProviderDescriptor.descriptor,
.mimo: MiMoProviderDescriptor.descriptor,
.doubao: DoubaoProviderDescriptor.descriptor,
.sakana: SakanaProviderDescriptor.descriptor,
.abacus: AbacusProviderDescriptor.descriptor,
.mistral: MistralProviderDescriptor.descriptor,
.deepseek: DeepSeekProviderDescriptor.descriptor,
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBarCore/Providers/Providers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public enum UsageProvider: String, CaseIterable, Sendable, Codable {
case perplexity
case mimo
case doubao
case sakana
case abacus
case mistral
case deepseek
Expand Down Expand Up @@ -96,6 +97,7 @@ public enum IconStyle: String, Sendable, CaseIterable {
case perplexity
case mimo
case doubao
case sakana
case abacus
case mistral
case deepseek
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Foundation

public enum SakanaProviderDescriptor {
public static let descriptor: ProviderDescriptor = Self.makeDescriptor()

static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .sakana,
metadata: ProviderMetadata(
id: .sakana,
displayName: "Sakana AI",
sessionLabel: "5-hour",
weeklyLabel: "Weekly",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show Sakana AI usage",
cliName: "sakana",
defaultEnabled: false,
isPrimaryProvider: false,
usesAccountFallback: false,
browserCookieOrder: nil,
dashboardURL: "https://console.sakana.ai/billing",
statusPageURL: nil),
branding: ProviderBranding(
iconStyle: .sakana,
iconResourceName: "ProviderIcon-sakana",
color: ProviderColor(red: 0.16, green: 0.46, blue: 0.86)),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "Sakana AI cost summary is not supported." }),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .web],
Comment thread
LeoLin990405 marked this conversation as resolved.
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in
[SakanaWebFetchStrategy()]
})),
cli: ProviderCLIConfig(
name: "sakana",
aliases: ["sakana-ai"],
versionDetector: nil))
}
}

struct SakanaWebFetchStrategy: ProviderFetchStrategy {
let id: String = "sakana.web"
let kind: ProviderFetchKind = .web

func isAvailable(_ context: ProviderFetchContext) async -> Bool {
SakanaSettingsReader.cookieHeader(environment: context.env) != nil
}

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
guard let cookieHeader = SakanaSettingsReader.cookieHeader(environment: context.env) else {
throw SakanaUsageError.missingCookie
}
let usage = try await SakanaUsageFetcher.fetchUsage(
cookieHeader: cookieHeader,
timeout: context.webTimeout)
return self.makeResult(usage: usage.toUsageSnapshot(), sourceLabel: "web")
}

func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
false
}
}
22 changes: 22 additions & 0 deletions Sources/CodexBarCore/Providers/Sakana/SakanaSettingsReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation

public enum SakanaSettingsReader {
public static let cookieHeaderKey = "SAKANA_COOKIE"

public static func cookieHeader(environment: [String: String] = ProcessInfo.processInfo.environment) -> String? {
CookieHeaderNormalizer.normalize(self.cleaned(environment[self.cookieHeaderKey]))
}

static func cleaned(_ raw: String?) -> String? {
guard var value = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
return nil
}
if (value.hasPrefix("\"") && value.hasSuffix("\"")) ||
(value.hasPrefix("'") && value.hasSuffix("'"))
{
value = String(value.dropFirst().dropLast())
}
value = value.trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : value
}
}
Loading